diff --git a/package.json b/package.json
index 824851d..2f1c070 100644
--- a/package.json
+++ b/package.json
@@ -100,8 +100,10 @@
"@types/wicg-file-system-access": "^2020.9.1",
"@webcomponents/webcomponentsjs": "^2.5.0",
"clone-deep": "^4.0.1",
+ "electron-context-menu": "^2.4.0",
"electron-squirrel-startup": "^1.0.0",
"hast-util-to-html": "^7.1.2",
+ "immer": "^8.0.1",
"lit-element": "^2.4.0",
"lit-html": "^1.3.0",
"mdast-util-compact": "^3.0.0",
diff --git a/src/ast/backspace-paragraph.ts b/src/ast/backspace-paragraph.ts
new file mode 100644
index 0000000..559a22c
--- /dev/null
+++ b/src/ast/backspace-paragraph.ts
@@ -0,0 +1,36 @@
+import {visit, EXIT} from "unist-utils-core"
+import * as md from "mdast"
+import compact from "mdast-util-compact"
+
+export default function backspaceParagraph(
+ root: md.Parent,
+ target: md.Paragraph,
+ id?: string
+): void {
+ visit(root, target, (node, index, parents) => {
+ let parent = parents[parents.length - 1]
+
+ let previous = parent.children[index - 1]
+ if (!previous) {
+ throw new RangeError(
+ "can't merge backwards when there is nothing behind"
+ )
+ }
+
+ if (Array.isArray(previous.children)) {
+ let length = previous.children.length
+ previous.children.splice(length, 0, ...node.children)
+ parent.children[index - 1] = compact(previous)
+ // TODO come back and use merge
+ if (id) {
+ let child = previous.children[length - 1]
+ child.id = id
+ }
+ } else {
+ throw new TypeError("line before somehow has no children?")
+ }
+
+ parent.children.splice(index, 1)
+ return EXIT
+ })
+}
diff --git a/src/ast/convert-to-html.ts b/src/ast/convert-to-html.ts
index 9d4abe3..75cf103 100644
--- a/src/ast/convert-to-html.ts
+++ b/src/ast/convert-to-html.ts
@@ -2,22 +2,15 @@ import type * as md from "mdast"
import {html, TemplateResult} from "lit-html"
import {spread} from "@open-wc/lit-helpers"
import * as is from "./is"
+import {DataCaret} from "../caret"
-type Handler = (node: md.Content | md.Root, parent: md.Parent | md.Root) => any
+type Handler = (node: md.Content) => any
interface Handlers {
[type: string]: Handler
}
-function children(node: md.Parent) {
- return html`${node.children.map(n => convertToHtml(n, node))}`
-}
-
-function value(node: md.Literal) {
- return html`${node.value}`
-}
-
-function spreadable(node: md.Content | md.Root) {
+export function spreadable(node: md.Content | md.Root) {
let {data, position, children, ...spreadable} = node
if (is.leaf(node.type)) {
spreadable.leaf = true
@@ -34,21 +27,25 @@ function spreadable(node: md.Content | md.Root) {
if (is.empty(node.type)) {
spreadable.empty = true
}
+
+ if (data && data.caret) {
+ let caret: DataCaret = data.caret as DataCaret
+ spreadable.caret = caret.type
+ spreadable.caretOffset = caret.offset
+ }
return spreadable
}
let handlers: Handlers = {
- blockquote(node, parent) {
+ blockquote(node) {
node = node as md.Blockquote
return html`
- ${children(node)}
- `
+ >`
},
- break(node, parent) {
+ break(node) {
node = node as md.Break
return html``
},
- code(node, parent) {
+ code(node) {
node = node as md.Code
- return html`${value(node)}`
+ return html``
},
- delete(node, parent) {
+ delete(node) {
node = node as md.Delete
- return html`${children(node)}`
+ return html``
},
- emphasis(node, parent) {
+ emphasis(node) {
node = node as md.Emphasis
- return html`${children(node)}`
+ return html``
},
- footnoteReference(node, parent) {
+ footnoteReference(node) {
node = node as md.FootnoteReference
return html``
},
- footnote(node, parent) {
+ footnote(node) {
node = node as md.Footnote
- return html`${children(node)}`
+ return html``
},
- heading(node, parent) {
+ heading(node) {
node = node as md.Heading
let h
switch (node.depth) {
case 1:
- h = html`
${children(node)}
`
+ h = html``
break
case 2:
- h = html`${children(node)}
`
+ h = html``
break
case 3:
- h = html`${children(node)}
`
+ h = html``
break
case 4:
- h = html`${children(node)}
`
+ h = html``
break
case 5:
- h = html`${children(node)}
`
+ h = html``
break
case 6:
- h = html`${children(node)}
`
+ h = html``
break
}
- return html`${children(node)}`
+ return html``
},
- html(node, parent) {
+ html(node) {
node = node as md.HTML
- return html`${value(node)}`
+ return html``
},
- imageReference(node, parent) {
+ imageReference(node) {
node = node as md.ImageReference
return html``
},
- image(node, parent) {
+ image(node) {
node = node as md.Image
- return html``
+ >`
},
- inlineCode(node, parent) {
+ inlineCode(node) {
node = node as md.InlineCode
return html`${value(node)}`
+ >`
},
- linkReference(node, parent) {
+ linkReference(node) {
node = node as md.LinkReference
return html`${children(node)}`
+ >`
},
- link(node, parent) {
+ link(node) {
node = node as md.Link
- return html`${children(node)}`
+ return html``
},
- listItem(node, parent) {
+ listItem(node) {
node = node as md.ListItem
- return html`${children(node)}`
+ return html``
},
- list(node, parent) {
+ list(node) {
node = node as md.List
- return html`${children(node)}`
+ return html``
},
- paragraph(node, parent) {
+ paragraph(node) {
node = node as md.Paragraph
- return html`${children(node)}`
+ return html``
},
- root(node, parent) {
- node = node as md.Root
- /* return html`
- ${children(node)}
+ // root(node) {
+ //node = node as md.Root
+ /* return html`
+
` */
- return children(node)
- },
+ //return children(node)
+ // },
- strong(node, parent) {
+ strong(node) {
node = node as md.Strong
- return html`${children(node)}`
+ return html``
},
- table(node, parent) {
+ table(node) {
node = node as md.Table
- return html`${children(node)}`
+ return html``
},
- text(node, parent) {
+ text(node) {
// You probably don't want this.
// remember: if you enable this, every text (including those inside an )
// becomes a mayo- element
@@ -227,7 +234,7 @@ let handlers: Handlers = {
return node.value
},
- thematicBreak(node, parent) {
+ thematicBreak(node) {
node = node as md.ThematicBreak
return html``
},
- toml(node, parent) {
+ toml(node) {
node = node as md.FrontmatterContent
- return html`${value(node)}`
+ return html``
},
- yaml(node, parent) {
+ yaml(node) {
node = node as md.FrontmatterContent
- return html`${value(node)}`
+ return html``
},
- definition(node, parent) {
+ definition(node) {
node = node as md.Definition
return html``
},
- footnoteDefinition(node, parent) {
+ footnoteDefinition(node) {
node = node as md.FootnoteDefinition
- return html`${children(node)}`
+ return html``
},
}
export default function convertToHtml(
- node: md.Content | md.Root,
- parent: md.Parent | md.Root
+ node: md.Content
): TemplateResult | string {
let handler = handlers[node.type]
if (handler) {
- return handler(node, parent)
+ return handler(node)
} else {
throw new Error(node.type)
}
diff --git a/src/ast/insert-paragraph.ts b/src/ast/insert-paragraph.ts
new file mode 100644
index 0000000..a42cd2b
--- /dev/null
+++ b/src/ast/insert-paragraph.ts
@@ -0,0 +1,68 @@
+import {visit, EXIT, selectAll} from "unist-utils-core"
+import * as unist from "unist"
+import * as md from "mdast"
+import u from "unist-builder"
+import * as is from "./is"
+import {remove} from "./utils"
+import split from "./split"
+
+export default function insertParagraph(
+ root: md.Parent,
+ target: md.Text | md.InlineCode,
+ offset: number,
+ id?: string
+): void {
+ if (!target || (target.type != "text" && target.type != "inlineCode")) {
+ throw new Error(
+ `must have a text or inlineCode target, got ${target?.type}`
+ )
+ }
+ let [left, right] = split(root, target, offset)
+ let t = selectAll("text, inlineCode", right)
+ if (t.length == 1) {
+ let [node] = t
+ if (node.value.length == 0) {
+ node.type = "text"
+ node.value = " "
+ }
+ }
+
+ visit(root, target, (node, index, parents) => {
+ node.value = left.value
+ let firstp = parents[parents.length - 1]
+ let prest = firstp.children.slice(index + 1)
+
+ let rest: unist.Node[] = []
+ if (target.type == "text" || target.type == "inlineCode") {
+ parents.forEach((parent, index) => {
+ if (is.inline(parent.type)) {
+ let idx = parents[index - 1].children.indexOf(parent)
+ parents[index - 1].children.splice(idx, 1, left)
+ if (!rest.length) {
+ rest = parents[index - 1].children.slice(idx + 1)
+ }
+ rest.forEach((n: unist.Node) => remove(parents[index - 1], {}, n))
+ }
+ })
+ }
+ parents.forEach((parent, index) => {
+ if (is.block(parent.type) && is.leaf(parent.type)) {
+ let idx = parents[index - 1].children.indexOf(parent)
+ let opts: {id?: string} = {}
+ if (id) {
+ opts.id = id
+ }
+ prest.forEach((n: unist.Node) => remove(parents[index - 1], {}, n))
+ if (parent != firstp) {
+ prest = []
+ }
+ parents[index - 1].children.splice(
+ idx + 1,
+ 0,
+ u("paragraph", opts, [right, ...rest, ...prest])
+ )
+ }
+ })
+ return EXIT
+ })
+}
diff --git a/src/ast/split.ts b/src/ast/split.ts
new file mode 100644
index 0000000..015d4f6
--- /dev/null
+++ b/src/ast/split.ts
@@ -0,0 +1,43 @@
+import {visit, EXIT} from "unist-utils-core"
+import * as unist from "unist"
+import * as md from "mdast"
+import u from "unist-builder"
+import * as is from "./is"
+
+export default function split(
+ root: md.Parent,
+ target: md.Text | md.InlineCode,
+ offset: number
+): [unist.Node, unist.Node] {
+ let leftText = target.value.slice(0, offset)
+ let rightText = target.value.slice(offset)
+
+ let leftNode: unist.Node = u(target.type, leftText)
+ let rightNode: unist.Node = u(target.type, rightText)
+
+ visit(root, target, (node, index, parents) => {
+ let firstp = parents[parents.length - 1]
+ let nextpc = firstp.children.slice(0, index)
+ let nextac = firstp.children.slice(index + 1)
+ for (let i = parents.length - 1; i > 0; i--) {
+ let highParent = parents[i - 1]
+ let lowParent = parents[i]
+ let idx = highParent.children.indexOf(lowParent)
+ let pc = highParent.children.slice(0, idx)
+ let ac = highParent.children.slice(idx + 1)
+ if (is.inline(lowParent.type)) {
+ leftNode = u(lowParent.type, [...nextpc, leftNode])
+ rightNode = u(lowParent.type, [rightNode, ...nextac])
+ }
+ nextpc = pc
+ nextac = ac
+ if (is.block(lowParent.type)) {
+ break
+ }
+ }
+
+ return EXIT
+ })
+
+ return [leftNode, rightNode]
+}
diff --git a/src/ast/transform/index.ts b/src/ast/transform/index.ts
new file mode 100644
index 0000000..3ea2167
--- /dev/null
+++ b/src/ast/transform/index.ts
@@ -0,0 +1,281 @@
+import * as md from "mdast"
+import {CaretIdInstruction} from "../caret"
+import BeforeInputEvent, {InputType} from "../before-input-event"
+import insertText from "./insert-text"
+import visit from "unist-util-visit"
+import type * as unist from "unist"
+import {produce} from "immer"
+
+export interface TransformOptions {
+ data: string
+ inputType: InputType
+ start: {
+ path: string
+ offset: number
+ }
+ end: {
+ path: string
+ offset: number
+ }
+}
+
+export let nothing = Symbol("@transform/don't-actually")
+
+type TransformResult = CaretIdInstruction | typeof nothing
+
+interface TransformHandlers {
+ [key: string]: TransformHandler
+}
+
+export interface TransformHandler {
+ (root: md.Root, options: TransformHandlerOptions): TransformResult
+}
+
+export let handlers: TransformHandlers = {
+ insertReplacementText(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ insertText,
+ insertLineBreak(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ insertParagraph(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ deleteContentBackward(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ deleteContentForward(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ deleteByCut(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ insertOrderedList(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ insertUnorderedList(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ insertHorizontalRule(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ insertFromYank(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ insertFromDrop(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ insertFromPaste(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ insertFromPasteAsQuotation(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ insertTranspose(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ insertCompositionText(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ insertLink(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ deleteWordBackward(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ deleteWordForward(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ deleteSoftLineBackward(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ deleteSoftLineForward(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ deleteEntireSoftLine(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ deleteHardLineBackward(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ deleteHardLineForward(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ deleteByDrag(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ deleteContent(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ historyUndo(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ historyRedo(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatBold(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatItalic(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatUnderline(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatStrikeThrough(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatSuperscript(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatSubscript(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatJustifyFull(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatJustifyCenter(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatJustifyRight(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatJustifyLeft(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatIndent(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatOutdent(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatRemove(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatSetBlockTextDirection(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatSetInlineTextDirection(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatBackColor(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatFontColor(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+ formatFontName(root, options) {
+ console.error(`unhandled inputType`)
+ return nothing
+ },
+}
+
+export interface TransformHandlerOptions {
+ data: string
+ start: {
+ node: md.Content
+ offset: number
+ }
+ end: {
+ node: md.Content
+ offset: number
+ }
+}
+
+export default function transform(
+ root: md.Root,
+ options: TransformOptions
+): void {
+ let handler = handlers[options.inputType]
+
+ if (!handler) {
+ throw new Error(`unknown inputType`)
+ }
+
+ visit(root, node => {
+ if (node.data && node.data.caret) {
+ delete node.data.caret
+ } else if (!node.data) {
+ node.data = {}
+ }
+ })
+
+ let startParts = options.start.path.slice(1).split(".").map(Number)
+ console.log(startParts)
+ let startNode: unist.Node = root
+
+ while (startParts.length) {
+ if (!Array.isArray(startNode.children)) {
+ throw new TypeError("no chilcren")
+ }
+ let idx = startParts.shift()
+ startNode = startNode.children[idx]
+ }
+ let endParts = options.end.path.slice(1).split(".").map(Number)
+ let endNode: unist.Node = root
+ while (endParts.length) {
+ if (!Array.isArray(endNode.children)) {
+ throw new TypeError("no chilcren")
+ }
+ endNode = endNode.children[endParts.shift()]
+ }
+
+ let opts = {
+ data: options.data,
+ start: {
+ node: startNode as md.Content,
+ offset: options.start.offset,
+ },
+ end: {
+ node: endNode as md.Content,
+ offset: options.end.offset,
+ },
+ }
+
+ handler(root, opts)
+}
diff --git a/src/ast/transform/insert-text.ts b/src/ast/transform/insert-text.ts
new file mode 100644
index 0000000..89e22f9
--- /dev/null
+++ b/src/ast/transform/insert-text.ts
@@ -0,0 +1,22 @@
+import type {TransformHandler} from "."
+import {visit} from "unist-utils-core"
+import {produce} from "immer"
+
+let insertText: TransformHandler = (root, options) => {
+ let startNode = options.start.node
+ let endNode = options.end.node
+ if (startNode == endNode && typeof startNode.value == "string") {
+ if (!startNode.data) {
+ startNode.data = {}
+ }
+ startNode.data.caret = {
+ type: "collapsed",
+ offset: options.start.offset + 1,
+ }
+ startNode.value =
+ startNode.value.slice(0, options.start.offset) +
+ options.data +
+ startNode.value.slice(options.end.offset)
+ }
+}
+export default insertText
diff --git a/src/ast/unwrap.ts b/src/ast/unwrap.ts
new file mode 100644
index 0000000..fde5655
--- /dev/null
+++ b/src/ast/unwrap.ts
@@ -0,0 +1,45 @@
+import {visit, EXIT} from "unist-utils-core"
+import * as unist from "unist"
+import * as md from "mdast"
+import u from "unist-builder"
+import * as is from "./is"
+
+export type Unwrappable = md.Emphasis | md.Strong | md.Delete | md.InlineCode
+
+export default function unwrap(
+ root: md.Parent,
+ target: md.InlineCode,
+ offset: number
+): [unist.Node, unist.Node] {
+ let leftText = target.value.slice(0, offset)
+ let rightText = target.value.slice(offset)
+
+ let leftNode: unist.Node = u(target.type, leftText)
+ let rightNode: unist.Node = u(target.type, rightText)
+
+ visit(root, target, (node, index, parents) => {
+ let firstp = parents[parents.length - 1]
+ let nextpc = firstp.children.slice(0, index)
+ let nextac = firstp.children.slice(index + 1)
+ for (let i = parents.length - 1; i > 0; i--) {
+ let highParent = parents[i - 1]
+ let lowParent = parents[i]
+ let idx = highParent.children.indexOf(lowParent)
+ let pc = highParent.children.slice(0, idx)
+ let ac = highParent.children.slice(idx + 1)
+ if (is.inline(lowParent.type)) {
+ leftNode = u(lowParent.type, [...nextpc, leftNode])
+ rightNode = u(lowParent.type, [rightNode, ...nextac])
+ }
+ nextpc = pc
+ nextac = ac
+ if (is.block(lowParent.type)) {
+ break
+ }
+ }
+
+ return EXIT
+ })
+
+ return [leftNode, rightNode]
+}
diff --git a/src/ast/utils.test.ts b/src/ast/utils.test.ts
deleted file mode 100644
index 7e26eaa..0000000
--- a/src/ast/utils.test.ts
+++ /dev/null
@@ -1,198 +0,0 @@
-import * as utils from "./utils"
-
-import fromMarkdown from "mdast-util-from-markdown"
-import toMarkdown from "mdast-util-to-markdown"
-import {select, selectAll} from "unist-utils-core"
-import * as md from "mdast"
-
-import removePosition from "unist-util-remove-position"
-
-describe("split", () => {
- test("splits an ordinary text node", () => {
- let tree = parse("hellothere")
- let expected = [
- parse(`hello`).children[0].children[0],
- parse(`there`).children[0].children[0],
- ]
- let p = select("paragraph", tree)
- let text = select("text", p)
- let actual = utils.split(tree, text, 5)
-
- expect(toMarkdown(actual[0])).toEqual(toMarkdown(expected[0]))
- expect(toMarkdown(actual[1])).toEqual(toMarkdown(expected[1]))
- expect(actual).toEqual(expected)
- })
- test("splits a node in half", () => {
- let tree = parse("_hellothere_")
- let expected = [
- parse(`_hello_`).children[0].children[0],
- parse(`_there_`).children[0].children[0],
- ]
- let p = select("paragraph", tree)
- let text = select("text", p)
- let actual = utils.split(tree, text, 5)
- expect(actual).toEqual(expected)
- })
-
- it("works in a heading", () => {
- let tree = parse("# well, well, well")
- let expected = [
- parse(`# well, well`).children[0].children[0],
- parse(`, well`).children[0].children[0],
- ]
- let h1 = select("heading", tree)
- let text = select("text", h1)
- let actual = utils.split(tree, text, 10)
- expect(actual).toEqual(expected)
- })
-
- test("splits a complex node correct", () => {
- let tree = parse("_oh **hey** ok_")
- let expected = [
- parse(`_oh **he**_`).children[0].children[0],
- parse(`_**y** ok_`).children[0].children[0],
- ]
- let p = select("paragraph", tree)
- let text = selectAll("text", p)[1]
- let actual = utils.split(tree, text, 2)
-
- expect(toMarkdown(actual[0])).toEqual(toMarkdown(expected[0]))
- expect(toMarkdown(actual[1])).toEqual(toMarkdown(expected[1]))
- expect(actual).toEqual(expected)
- })
-
- test("splits a code node correct", () => {
- let tree = parse("_oh **hello, `here` is it** ok_")
- let expected = [
- parse(`_oh **hello, \`he\`**_`).children[0].children[0],
- parse(`_**\`re\` is it** ok_`).children[0].children[0],
- ]
- let p = select("paragraph", tree)
- let text = select("inlineCode", p)
-
- let actual = utils.split(tree, text, 2)
-
- expect(toMarkdown(actual[0])).toEqual(toMarkdown(expected[0]))
- expect(toMarkdown(actual[1])).toEqual(toMarkdown(expected[1]))
- expect(actual).toEqual(expected)
- })
-
- test("returns only the split node when there are other children", () => {
- let tree = parse("one two **three** four")
- let expected = [
- parse(`**thr**`).children[0].children[0],
- parse(`**ee**`).children[0].children[0],
- ]
- let p = select("paragraph", tree)
- let text = selectAll("text", p)[1]
-
- let actual = utils.split(tree, text, 3)
-
- expect(toMarkdown(actual[0])).toEqual(toMarkdown(expected[0]))
- expect(toMarkdown(actual[1])).toEqual(toMarkdown(expected[1]))
- expect(actual).toEqual(expected)
- })
-})
-
-function parse(string: string): md.Root {
- return removePosition(fromMarkdown(string), true) as md.Root
-}
-
-describe("insertParagraph", () => {
- test("inserts a new paragraph where the cursor is", () => {
- let tree = parse("hellothere")
- let expected = parse(`hello
-
-there`)
- let p = select("paragraph", tree)
- let text = select("text", p)
- utils.insertParagraph(tree, text, 5)
-
- expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
- expect(tree).toEqual(expected)
- })
-
- test("inserts a new paragraph where the cursor is (in a code)", () => {
- let tree = parse("`hellothere`")
- let expected = parse(`\`hello\`
-
-\`there\``)
- let p = select("paragraph", tree)
- let text = select("inlineCode", p)
- utils.insertParagraph(tree, text, 5)
-
- expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
- expect(tree).toEqual(expected)
- })
-
- test("inserts a new paragraph where the cursor is (in a complex place)", () => {
- let tree = parse(
- "this is *story __all about how my life__ got flipped* turned up side down"
- )
- let expected = parse(
- `this is *story __all ab__*
-
-*__out how my life__ got flipped* turned up side down`
- )
- let p = select("paragraph", tree)
- let text = selectAll("text", p)[2]
- utils.insertParagraph(tree, text, 6)
-
- expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
- expect(tree).toEqual(expected)
- })
-
- test("inserts a new paragraph where the cursor is (in a complex place in a code)", () => {
- let tree = parse(
- "this is *story __all about `how` my life__ got flipped* turned up side down"
- )
- let expected = parse(
- `this is *story __all about \`h\`__*
-
-*__\`ow\` my life__ got flipped* turned up side down`
- )
- let p = select("paragraph", tree)
- let inlineCode = select("inlineCode", p)
- utils.insertParagraph(tree, inlineCode, 1)
-
- expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
- expect(tree).toEqual(expected)
- })
-
- test("inserts a new paragraph, maintaining whole nodes", () => {
- let tree = parse("split __mein two__ please")
- let expected = parse(`split __me__
-
-__in two__ please`)
- let p = select("paragraph", tree)
- let text = selectAll("text", p)[1]
-
- utils.insertParagraph(tree, text, 2)
-
- expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
- expect(tree).toEqual(expected)
- })
-
- it("works in a heading", () => {
- let tree = parse("# well, well, well")
- let expected = parse(`# well, well
-
-, well`)
- let h1 = select("heading", tree)
- let text = select("text", h1)
- utils.insertParagraph(tree, text, 10)
- expect(tree).toEqual(expected)
- })
-
- it("works in a heading with special elements", () => {
- let tree = parse("# well, _well_, well, well")
- let expected = parse(`# we
-
-ll, _well_, well, well`)
- let h1 = select("heading", tree)
- let text = selectAll("text", h1)[0]
- utils.insertParagraph(tree, text, 2)
- expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
- expect(tree).toEqual(expected)
- })
-})
diff --git a/src/ast/utils.ts b/src/ast/utils.ts
index 8b1183a..a8668c9 100644
--- a/src/ast/utils.ts
+++ b/src/ast/utils.ts
@@ -1,109 +1,8 @@
-import {find, visit, EXIT, selectAll} from "unist-utils-core"
+import {find} from "unist-utils-core"
// @ts-ignore
import uremove from "unist-util-remove"
import * as unist from "unist"
import * as md from "mdast"
-import u from "unist-builder"
-import * as is from "./is"
-
-export function insertParagraph(
- root: md.Parent,
- target: md.Text | md.InlineCode,
- offset: number,
- id?: string
-): void {
- if (!target || (target.type != "text" && target.type != "inlineCode")) {
- throw new Error(
- `must have a text or inlineCode target, got ${target?.type}`
- )
- }
- let [left, right] = split(root, target, offset)
- let t = selectAll("text, inlineCode", right)
- if (t.length == 1) {
- let [node] = t
- if (node.value.length == 0) {
- node.type = "text"
- node.value = " "
- }
- }
-
- visit(root, target, (node, index, parents) => {
- node.value = left.value
- let firstp = parents[parents.length - 1]
- let prest = firstp.children.slice(index + 1)
-
- let rest: unist.Node[] = []
- if (target.type == "text" || target.type == "inlineCode") {
- parents.forEach((parent, index) => {
- if (is.inline(parent.type)) {
- let idx = parents[index - 1].children.indexOf(parent)
- parents[index - 1].children.splice(idx, 1, left)
- if (!rest.length) {
- rest = parents[index - 1].children.slice(idx + 1)
- }
- rest.forEach((n: unist.Node) => remove(parents[index - 1], {}, n))
- }
- })
- }
- parents.forEach((parent, index) => {
- if (is.block(parent.type) && is.leaf(parent.type)) {
- let idx = parents[index - 1].children.indexOf(parent)
- let opts: {id?: string} = {}
- if (id) {
- opts.id = id
- }
- prest.forEach((n: unist.Node) => remove(parents[index - 1], {}, n))
- if (parent != firstp) {
- prest = []
- }
- parents[index - 1].children.splice(
- idx + 1,
- 0,
- u("paragraph", opts, [right, ...rest, ...prest])
- )
- }
- })
- return EXIT
- })
-}
-
-export function split(
- root: md.Parent,
- target: md.Text | md.InlineCode,
- offset: number
-): [unist.Node, unist.Node] {
- let leftText = target.value.slice(0, offset)
- let rightText = target.value.slice(offset)
-
- let leftNode: unist.Node = u(target.type, leftText)
- let rightNode: unist.Node = u(target.type, rightText)
-
- visit(root, target, (node, index, parents) => {
- let firstp = parents[parents.length - 1]
- let nextpc = firstp.children.slice(0, index)
- let nextac = firstp.children.slice(index + 1)
- for (let i = parents.length - 1; i > 0; i--) {
- let highParent = parents[i - 1]
- let lowParent = parents[i]
- let idx = highParent.children.indexOf(lowParent)
- let pc = highParent.children.slice(0, idx)
- let ac = highParent.children.slice(idx + 1)
- if (is.inline(lowParent.type)) {
- leftNode = u(lowParent.type, [...nextpc, leftNode])
- rightNode = u(lowParent.type, [rightNode, ...nextac])
- }
- nextpc = pc
- nextac = ac
- if (is.block(lowParent.type)) {
- break
- }
- }
-
- return EXIT
- })
-
- return [leftNode, rightNode]
-}
export function toString(node: unist.Node): string {
return (
diff --git a/src/before-input-event.ts b/src/before-input-event.ts
new file mode 100644
index 0000000..ee0cde5
--- /dev/null
+++ b/src/before-input-event.ts
@@ -0,0 +1,53 @@
+export type InputType =
+ | "insertReplacementText"
+ | "insertText"
+ | "insertLineBreak"
+ | "insertParagraph"
+ | "deleteContentBackward"
+ | "deleteContentForward"
+ | "deleteByCut"
+ | "insertOrderedList"
+ | "insertUnorderedList"
+ | "insertHorizontalRule"
+ | "insertFromYank"
+ | "insertFromDrop"
+ | "insertFromPaste"
+ | "insertFromPasteAsQuotation"
+ | "insertTranspose"
+ | "insertCompositionText"
+ | "insertLink"
+ | "deleteWordBackward"
+ | "deleteWordForward"
+ | "deleteSoftLineBackward"
+ | "deleteSoftLineForward"
+ | "deleteEntireSoftLine"
+ | "deleteHardLineBackward"
+ | "deleteHardLineForward"
+ | "deleteByDrag"
+ | "deleteContent"
+ | "historyUndo"
+ | "historyRedo"
+ | "formatBold"
+ | "formatItalic"
+ | "formatUnderline"
+ | "formatStrikeThrough"
+ | "formatSuperscript"
+ | "formatSubscript"
+ | "formatJustifyFull"
+ | "formatJustifyCenter"
+ | "formatJustifyRight"
+ | "formatJustifyLeft"
+ | "formatIndent"
+ | "formatOutdent"
+ | "formatRemove"
+ | "formatSetBlockTextDirection"
+ | "formatSetInlineTextDirection"
+ | "formatBackColor"
+ | "formatFontColor"
+ | "formatFontName"
+
+export default interface BeforeInputEvent extends InputEvent {
+ getTargetRanges(): StaticRange[]
+ dataTransfer: DataTransfer
+ inputType: InputType
+}
diff --git a/src/default-doc.ts b/src/default-doc.ts
new file mode 100644
index 0000000..dc5176d
--- /dev/null
+++ b/src/default-doc.ts
@@ -0,0 +1,35 @@
+export default `# hello \`this\` and _that_ (and \`others\`)
+
+this is an _ordinary **\`document\` about** ordinary_ things, there's **nothing _going_ on**
+here of _interest to you_, or me, or anybody else.
+
+
+## a list
+
+- one
+- two
+- three
+
+1. first thing
+2. second
+3. the aeroplane
+
+## the other thing
+
+- one
+
+images like ![dog in hat](https://i.pinimg.com/originals/c1/40/6f/c1406f93f49e896ff7c54c26bbfda047.jpg) and so on
+
+> help
+> this is why i need help
+
+\`\`\`cpp filename="hello"
+auto sum(std::vector nums) {
+ auto result = 0;
+ for (auto num : nums) {
+ result += num;
+ }
+ return result;
+}
+\`\`\`
+`
diff --git a/src/electron/main.ts b/src/electron/main.ts
index 08ba6bd..b2b9876 100644
--- a/src/electron/main.ts
+++ b/src/electron/main.ts
@@ -1,16 +1,43 @@
-import {app, BrowserWindow} from "electron"
+import {app, BrowserWindow, shell} from "electron"
+import contextMenu from "electron-context-menu"
declare const MAIN_WINDOW_WEBPACK_ENTRY: any
if (require("electron-squirrel-startup")) {
app.quit()
}
+// TODO add emph, strong, link etc etc
+contextMenu({
+ prepend: (defaultActions, params, browserWindow) => [
+ // {
+ // label: "Rainbow",
+ // // Only show it when right-clicking images
+ // visible: params.mediaType === "image",
+ // },
+ {
+ label: "Search DuckDuckGo for “{selection}”",
+ // Only show it when right-clicking text
+ visible: params.selectionText.trim().length > 0,
+ click: () => {
+ shell.openExternal(
+ `https://duckduckgo.com/?q=${encodeURIComponent(
+ params.selectionText
+ )}`
+ )
+ },
+ },
+ ],
+ showSearchWithGoogle: false,
+ showSaveImage: true,
+})
+
const createWindow = (): void => {
const mainWindow = new BrowserWindow({
height: 600,
width: 800,
webPreferences: {
nodeIntegration: true,
+ spellcheck: true,
},
})
diff --git a/src/elements/index.ts b/src/elements/index.ts
index 48b0eff..9ace88d 100644
--- a/src/elements/index.ts
+++ b/src/elements/index.ts
@@ -1,5 +1,3 @@
-import {MayoParentElement} from "./markdown/mayo-element"
-
import MayoBreakElement from "./markdown/mayo-break"
import MayoCodeElement from "./markdown/mayo-code"
import MayoInlineCodeElement from "./markdown/mayo-inline-code"
@@ -22,6 +20,8 @@ import MayoTextElement from "./markdown/mayo-text"
import MayoTomlElement from "./markdown/mayo-toml"
import MayoYamlElement from "./markdown/mayo-yaml"
+import MayoNodeElement from "./mayo-node"
+
import MayoKillerElement from "./mayo-killer"
import MayoAppElement from "./mayo-app"
@@ -62,7 +62,7 @@ export type MayoContentElement =
| MayoPhrasingContentElement
export type {
- MayoDocumentElement,
+ MayoDocumentElement as MayoDocumentElement,
MayoKillerElement,
MayoSidebarElement,
MayoSidebarFileElement,
@@ -91,6 +91,7 @@ export type {
}
export default [
+ MayoNodeElement,
MayoAppElement,
MayoDocumentElement,
MayoSidebarElement,
@@ -118,47 +119,3 @@ export default [
MayoTomlElement,
MayoYamlElement,
]
-
-export interface BeforeInputEvent extends InputEvent {
- getTargetRanges(): StaticRange[]
- dataTransfer: DataTransfer
-}
-
-export type CaretInstruction =
- | CaretIdInstruction
- | CaretParentInstruction
- | CaretTextInstruction
-
-export interface CaretIdInstruction {
- type: "id"
- // the id of the parent element to move to
- id: string
- // the index of the child in that parent element
- index: number
- // where the selection range should start
- startOffset: number
- // where the selection range should end
- endOffset?: number
-}
-
-export interface CaretParentInstruction {
- type: "parent"
- // the parent element to move to
- parent: MayoParentElement
- // the index of the child in that parent element
- index: number
- // where the selection range should start
- startOffset: number
- // where the selection range should end
- endOffset?: number
-}
-
-export interface CaretTextInstruction {
- type: "text"
- // the parent element to move to
- element: Text
- // where the selection range should start
- startOffset: number
- // where the selection range should end
- endOffset?: number
-}
diff --git a/src/elements/markdown/mayo-element.ts b/src/elements/markdown/mayo-element.ts
index 1d907bc..23bd16d 100644
--- a/src/elements/markdown/mayo-element.ts
+++ b/src/elements/markdown/mayo-element.ts
@@ -1,7 +1,8 @@
import type * as md from "mdast"
import type * as unist from "unist"
-import {split} from "../../ast/utils"
-import {CaretInstruction, MayoContentElement} from ".."
+import split from "../../ast/split"
+import {MayoContentElement} from ".."
+import {CaretInstruction} from "../../caret"
export abstract class MayoElement<
AstNodeType extends unist.Node
> extends HTMLElement {
@@ -139,28 +140,6 @@ export class MayoParentElement<
}
return caret
- // TODO replace this with code that creates a code node when you press `
- // let ticks = targetAstNode.value.match(/(\s)`([^`]+)`(\s)/)
-
- // if (ticks) {
- // let tickIndex = targetAstNode.value.indexOf(ticks[0])
- // let prespace = ticks[1]
- // let tickContent = ticks[2]
-
- // let aftspace = ticks[3]
- // this.node.children.splice(
- // targetIndex,
- // 1,
- // u("text", targetAstNode.value.slice(0, tickIndex) + prespace),
- // u("inlineCode", {id: shortId()}, tickContent),
- // u(
- // "text",
- // aftspace + targetAstNode.value.slice(tickIndex + ticks[0].length)
- // )
- // )
- // } else {
- // return caret
- // }
}
insertTextAsCommonAncestor(
@@ -205,7 +184,7 @@ export class MayoParentElement<
insertParagraph(range: StaticRange) {}
- selfDeleteContentBackward(range: StaticRange) {
+ selfDeleteContentBackward(range: StaticRange): CaretInstruction {
let targetTextNode = range.startContainer
let targetIndex = this.interestingChildren.indexOf(targetTextNode)
if (targetIndex == -1) {
diff --git a/src/elements/markdown/mayo-heading.ts b/src/elements/markdown/mayo-heading.ts
index b89f674..b11c04d 100644
--- a/src/elements/markdown/mayo-heading.ts
+++ b/src/elements/markdown/mayo-heading.ts
@@ -1,33 +1,50 @@
-import {CaretInstruction} from ".."
-import type * as md from "mdast"
-import {MayoParentElement} from "./mayo-element"
-import {property} from "lit-element"
+import {customElement, LitElement} from "lit-element"
+import {html} from "lit-html"
-export default class MayoHeadingElement extends MayoParentElement {
- get depth() {
- return this.node.depth
- }
-
- set depth(val: 1 | 2 | 3 | 4 | 5 | 6) {
- this.node.depth = val
- }
-
- selfInsertText(text: string, range: StaticRange): CaretInstruction {
- if (text == "#" && this.atBeginningOfBlock(range)) {
- if (this.depth < 6) {
- this.depth += 1
- }
- return {
- type: "parent",
- parent: this,
- index: 0,
- startOffset: 0,
- }
- }
- return super.selfInsertText(text, range)
- }
-
- connectedCallback(): void {
- super.connectedCallback()
+@customElement("mayo-heading")
+export default class MayoHeadingElement extends LitElement {
+ render() {
+ return html`hello`
}
+ // get depth(): Depth {
+ // return this.node.depth
+ // }
+ // set depth(val: Depth) {
+ // this.node.depth = val
+ // }
+ // selfInsertText(text: string, range: StaticRange): CaretInstruction {
+ // if (text == "#" && this.atBeginningOfBlock(range)) {
+ // if (this.depth < 6) {
+ // this.depth += 1
+ // }
+ // return {
+ // type: "parent",
+ // parent: this,
+ // index: 0,
+ // startOffset: 0,
+ // }
+ // }
+ // return super.selfInsertText(text, range)
+ // }
+ // selfDeleteContentBackward(range: StaticRange): CaretInstruction {
+ // if (this.atBeginningOfBlock(range)) {
+ // let id = shortid()
+ // this.node.id = id
+ // if (this.depth > 1) {
+ // this.depth -= 1
+ // } else {
+ // // @ts-ignore
+ // this.node.type = "paragraph"
+ // delete this.node.depth
+ // }
+ // return {
+ // type: "id",
+ // id,
+ // index: 0,
+ // startOffset: 0,
+ // }
+ // }
+ // let caret = super.selfDeleteContentBackward(range)
+ // return caret
+ // }
}
diff --git a/src/elements/markdown/mayo-list-item.ts b/src/elements/markdown/mayo-list-item.ts
index 30bc472..3e44a23 100644
--- a/src/elements/markdown/mayo-list-item.ts
+++ b/src/elements/markdown/mayo-list-item.ts
@@ -1,7 +1,23 @@
import {MayoParentElement} from "./mayo-element"
import * as md from "mdast"
+import shortid from "shortid"
+import {CaretInstruction} from "../../caret"
export default class MayoListItemElement extends MayoParentElement {
+ selfDeleteContentBackward(range: StaticRange): CaretInstruction {
+ if (this.atBeginningOfBlock(range)) {
+ let id = shortid()
+ this.node.id = id
+ return {
+ type: "id",
+ id,
+ index: 0,
+ startOffset: 0,
+ }
+ }
+ let caret = super.selfDeleteContentBackward(range)
+ return caret
+ }
connectedCallback() {
super.connectedCallback()
}
diff --git a/src/elements/markdown/mayo-list.ts b/src/elements/markdown/mayo-list.ts
index 65cbc27..2252cfd 100644
--- a/src/elements/markdown/mayo-list.ts
+++ b/src/elements/markdown/mayo-list.ts
@@ -1,7 +1,44 @@
import {MayoParentElement} from "./mayo-element"
import * as md from "mdast"
+import shortid from "shortid"
+import {CaretInstruction} from "../../caret"
export default class MayoListElement extends MayoParentElement {
+ selfDeleteContentBackward(range: StaticRange): CaretInstruction {
+ let targetNode = range.startContainer
+ let targetIndex = this.interestingChildren.indexOf(targetNode)
+ if (targetIndex == -1) {
+ throw new Error(`${targetNode} is not a child of ${this}`)
+ }
+
+ let targetAstNode = this.node.children[targetIndex]
+ let previousAstNode = this.node.children[targetIndex - 1]
+
+ if (previousAstNode) {
+ let index = previousAstNode.children.length
+ let [first] = targetAstNode.children
+ previousAstNode.children.splice(Infinity, 0, ...targetAstNode.children)
+ let id = shortid()
+ first.id = id
+
+ return {
+ type: "id",
+ id,
+ index: 0,
+ startOffset: 0,
+ }
+ } else {
+ // hello
+ return
+ }
+
+ if (this.atBeginningOfBlock(range)) {
+ let id = shortid()
+ this.node.id = id
+ }
+ let caret = super.selfDeleteContentBackward(range)
+ return caret
+ }
connectedCallback() {
super.connectedCallback()
}
diff --git a/src/elements/markdown/mayo-paragraph.ts b/src/elements/markdown/mayo-paragraph.ts
index ca63270..da5c3cb 100644
--- a/src/elements/markdown/mayo-paragraph.ts
+++ b/src/elements/markdown/mayo-paragraph.ts
@@ -1,7 +1,7 @@
import {MayoParentElement} from "./mayo-element"
import * as md from "mdast"
import u from "unist-builder"
-import {CaretInstruction} from ".."
+import {CaretInstruction} from "../../caret"
import shortid from "shortid"
export default class MayoParagraphElement extends MayoParentElement {
diff --git a/src/elements/mayo-app.ts b/src/elements/mayo-app.ts
index 9683a66..eb5016f 100644
--- a/src/elements/mayo-app.ts
+++ b/src/elements/mayo-app.ts
@@ -1,4 +1,3 @@
-import {controller, target} from "@github/catalyst"
import type {
MayoDocumentElement,
MayoSidebarElement,
@@ -6,7 +5,16 @@ import type {
} from "."
import {promises as fs} from "fs"
import path from "path"
-
+import {
+ LitElement,
+ property,
+ customElement,
+ query,
+ css,
+ CSSResult,
+} from "lit-element"
+import {html, TemplateResult} from "lit-html"
+import defaultDoc from "../default-doc"
export interface GetFilesEvent extends CustomEvent {
detail: {
target: MayoSidebarTreeElement
@@ -20,11 +28,236 @@ export interface OpenFileEvent extends CustomEvent {
}
}
-@controller
-export default class MayoAppElement extends HTMLElement {
- @target sidebar: MayoSidebarElement
- @target document: MayoDocumentElement
+@customElement("mayo-app")
+export default class MayoAppElement extends LitElement {
+ @query("mayo-document")
+ editor: MayoDocumentElement
+ @query("mayo-sidebar")
+ sidebar: MayoSidebarElement
+ @property()
currentFilePath: string
+ @property()
+ currentFileContents: string = defaultDoc
+
+ static get styles(): CSSResult {
+ return css`
+ mayo-sidebar {
+ height: 100vh;
+ overflow: hidden;
+ }
+
+ mayo-document {
+ flex: 1;
+ border-left: 20px solid white;
+ width: 100%;
+ padding-bottom: 5em;
+ height: 100%;
+ overflow: auto;
+ }
+
+ mayo-document[dirty] {
+ border-left: 20px solid #ff2a50;
+ }
+ * {
+ box-sizing: border-box;
+ }
+
+ body {
+ margin: 0;
+ padding: 0;
+ }
+
+ mayo-app {
+ display: flex;
+ height: 100vh;
+ }
+
+ mayo-sidebar {
+ height: 100vh;
+ overflow: hidden;
+ }
+
+ mayo-document {
+ flex: 1;
+ border-left: 20px solid white;
+ width: 100%;
+ padding-bottom: 5em;
+ height: 100vh;
+ overflow: auto;
+ }
+
+ mayo-document[dirty] {
+ border-left: 20px solid #ff2a50;
+ }
+
+ mayo-document {
+ overflow: auto;
+ height: 100%;
+ }
+
+ mayo-document article > * + * {
+ margin-top: 1em;
+ }
+
+ mayo-document article {
+ width: 100%;
+ height: 100%;
+ font-size: 1.4em;
+ font-family: avenir next, sans-serif;
+ padding: 1em;
+ color: #000;
+ line-height: 1.2;
+ max-width: 80ex;
+ margin: auto;
+ margin-bottom: 2em;
+ }
+
+ mayo-document article:focus {
+ outline: none;
+ }
+
+ mayo-heading {
+ display: block;
+ font-weight: bolder;
+ }
+
+ h1 {
+ font-size: 2em;
+ }
+
+ h2 {
+ font-size: 1.6em;
+ }
+
+ h3 {
+ font-size: 1.4em;
+ }
+
+ h4 {
+ font-size: 1.25em;
+ }
+
+ h5 {
+ font-size: 1.15em;
+ }
+
+ h6 {
+ font-size: 1em;
+ }
+
+ h1::before,
+ h2::before,
+ h3::before,
+ h4::before,
+ h5::before,
+ h6::before {
+ font-family: ibm plex mono, monospace;
+ font-style: italic;
+ display: inline-block;
+ font-size: 0.8em;
+ color: #77c;
+ margin-right: 0.5em;
+ }
+
+ h1::before {
+ content: "# ";
+ }
+
+ h2::before {
+ content: "## ";
+ }
+
+ h3::before {
+ content: "### ";
+ }
+
+ h4::before {
+ content: "#### ";
+ }
+
+ h5::before {
+ content: "##### ";
+ }
+
+ h6::before {
+ content: "###### ";
+ }
+
+ mayo-break {
+ display: block;
+ }
+
+ mayo-paragraph {
+ display: block;
+ }
+
+ mayo-paragraph + mayo-paragraph {
+ margin-top: 1em;
+ }
+
+ mayo-list {
+ list-style-type: disc;
+ padding-left: 1em;
+ display: block;
+ }
+
+ mayo-list[ordered="true"] {
+ list-style-type: decimal;
+ padding-left: 1em;
+ }
+
+ mayo-list-item {
+ display: list-item;
+ }
+
+ [ordered="false"] > mayo-list-item::-moz-list-bullet {
+ color: #af2af0;
+ }
+
+ [ordered="true"] > mayo-list-item::-moz-list-bullet {
+ font-weight: 500;
+ }
+
+ mayo-code {
+ display: block;
+ white-space: pre;
+ font-family: IBM Plex Mono, monospace;
+ background: #ffe9ed;
+ color: #c36;
+ padding: 1em;
+ font-size: 0.9em;
+ margin-bottom: 1em;
+ overflow-x: scroll;
+ }
+
+ [type="text"] {
+ white-space: pre-wrap;
+ }
+
+ [type="inlineCode"] {
+ font-family: IBM Plex Mono, monospace;
+ background: #ffe9ed;
+ }
+
+ mayo-emphasis {
+ font-style: oblique;
+ }
+
+ mayo-strong {
+ font-weight: bolder;
+ }
+
+ li > [type="paragraph"] > p {
+ margin: 0;
+ }
+ blockquote {
+ display: flex;
+ border-left: 5px solid #3399ff;
+ padding-left: 5px;
+ }
+ `
+ }
+
async getFiles(event: GetFilesEvent): Promise {
event.detail.target.directory = (
await fs.readdir(path.resolve(event.detail.target.cwd), {
@@ -42,56 +275,34 @@ export default class MayoAppElement extends HTMLElement {
async openFile(event: OpenFileEvent): Promise {
this.currentFilePath = path.resolve(event.detail.cwd, event.detail.name)
let contents = await fs.readFile(this.currentFilePath, {encoding: "utf-8"})
- this.document.setAttribute("contents", contents)
+ this.currentFileContents = contents
+ this.editor.setAttribute("contents", contents)
}
async saveFile(): Promise {
- let contents = this.document.contents
+ let contents = this.editor.contents
if (this.currentFilePath) {
await fs.writeFile(this.currentFilePath, contents)
- this.document.removeAttribute("dirty")
+ this.editor.removeAttribute("dirty")
} else {
// TODO save as
}
}
- async connectedCallback(): Promise {
- this.document.contents = `# hello \`this\` and _that_ (and \`others\`)
-
-this is an _ordinary **\`document\` about** ordinary_ things, there's **nothing _going_ on**
-here of _interest to you_, or me, or anybody else.
-
-
-## a list
-
-- one
-- two
-- three
-
-1. first thing
-2. second
-3. the aeroplane
-
-## the other thing
-
-- one
-
-images like ![dog in hat](https://i.pinimg.com/originals/c1/40/6f/c1406f93f49e896ff7c54c26bbfda047.jpg) and so on
-
-> help
-> this is why i need help
-
-\`\`\`cpp filename="hello"
-auto sum(std::vector nums) {
- auto result = 0;
- for (auto num : nums) {
- result += num;
- }
- return result;
-}
-\`\`\`
-`
- let cwd = "/home/chee/docfiles/"
- this.sidebar.setAttribute("cwd", cwd)
+ render(): TemplateResult {
+ return html`
+
+
+ `
}
}
diff --git a/src/elements/mayo-document.ts b/src/elements/mayo-document.ts
index b7958aa..a8629f7 100644
--- a/src/elements/mayo-document.ts
+++ b/src/elements/mayo-document.ts
@@ -1,73 +1,84 @@
import parse from "mdast-util-from-markdown"
-import convertToHtml from "../ast/convert-to-html"
import type * as md from "mdast"
-import {target} from "@github/catalyst"
-import shortId from "shortid"
-import type {
- BeforeInputEvent,
- CaretInstruction,
- MayoContentElement,
-} from "./index"
-import {html, render} from "lit-html"
-import u from "unist-builder"
-import {
- MayoElement,
- MayoLiteralElement,
- MayoParentElement,
-} from "./markdown/mayo-element"
+import BeforeInputEvent from "../before-input-event"
+import {CaretInstruction} from "../caret"
+import {html} from "lit-html"
// TODO learn how to declare types for this
// @ts-ignore
import compact from "mdast-util-compact"
-import {insertParagraph} from "../ast/utils"
import toMarkdown from "mdast-util-to-markdown"
-
-function isTextOrInlineCode(
- node: MayoContentElement
-): node is MayoLiteralElement | MayoLiteralElement {
- return node.node.type == "inlineCode" || node.node.type == "text"
-}
-
-export default class MayoDocumentElement extends HTMLElement {
- @target document: HTMLElement
-
- get path(): string {
- return this.getAttribute("path") || ""
+import * as is from "../ast/is"
+import {css, CSSResult, customElement, LitElement, property} from "lit-element"
+import transform, {nothing} from "../ast/transform"
+import getTransformOptions from "../get-transform-options"
+import "./mayo-node"
+import MayoNodeElement from "./mayo-node"
+import {produce, enableAllPlugins} from "immer"
+import {spread} from "@open-wc/lit-helpers"
+import {spreadable} from "../ast/convert-to-html"
+
+enableAllPlugins()
+
+@customElement("mayo-document")
+export default class MayoDocumentElement extends LitElement {
+ @property()
+ contents: string
+ @property({type: Boolean})
+ dirty: boolean
+ node: md.Root
+
+ createRenderRoot(): this {
+ return this
}
- set path(value: string) {
- this.setAttribute("path", value)
- }
-
- get contents(): string {
- return this.getAttribute("contents") || ""
- }
+ static get styles(): CSSResult {
+ return css`
+ article {
+ white-space: pre-wrap;
+ }
- set contents(value: string) {
- this.setAttribute("contents", value)
- }
+ article > * + * {
+ margin-top: 1em;
+ }
- ast: md.Root
+ article {
+ width: 100%;
+ height: 100%;
+ font-size: 1.6em;
+ font-family: avenir next, sans-serif;
+ padding: 1em;
+ color: #000;
+ line-height: 1.2;
+ max-width: 80ex;
+ margin: auto;
+ margin-bottom: 2em;
+ }
- static get observedAttributes(): string[] {
- return ["contents", "path"]
+ article:focus {
+ outline: none;
+ }
+ `
}
- get interestingChildren(): Node[] {
- return Array.from(this.document.childNodes).filter(
- n => "node" in n || n.nodeName == "#text"
- )
+ attributeChangedCallback(name: string, before: string, now: string): void {
+ super.attributeChangedCallback(name, before, now)
+ if (name == "contents") {
+ this.node = parse(this.contents)
+ }
}
+
updateSelection(caret: CaretInstruction | null): void {
- let selection = document.getSelection()!
+ let selection = document.getSelection()
if (caret) {
let target: Text | null = null
if (caret && caret.type == "id") {
let parent = document.getElementById(
- caret.id
- ) as MayoParentElement
- target = parent.interestingChildren[caret.index] as Text
+ caret.elementId
+ ) as MayoNodeElement
} else if (caret && caret.type == "parent") {
- target = caret.parent.interestingChildren[caret.index] as Text
+ target = caret.parentElement.interestingChildren[
+ caret.textChildIndex
+ ] as Text
} else if (caret && caret.type == "text") {
target = caret.element
}
@@ -77,245 +88,178 @@ export default class MayoDocumentElement extends HTMLElement {
range.setStart(target, caret ? caret.startOffset : 0)
selection.addRange(range)
}
- } else {
- // this.straightUpWingTheSelection()
}
}
- handleInput(event: BeforeInputEvent) {
+ async handleInput(event: BeforeInputEvent) {
+ event.preventDefault()
// TODO multiple ranges?
- let range: StaticRange = event.getTargetRanges()[0]
- let isCaret =
- range.endContainer == range.startContainer &&
- range.startOffset == range.endOffset
- let isRange = !isCaret
-
- let startElement = range.startContainer
- .parentElement! as MayoContentElement
- let endElement = range.endContainer.parentElement! as MayoContentElement
-
- let startNode: md.Content = startElement.node
- let endNode: md.Content = startElement.node
-
- let startBlockElement = startElement.block
- ? startElement
- : (startElement.closest("[block]")! as MayoParentElement)
-
- let endBlockElement = endElement.block
- ? endElement
- : (endElement.closest("[block]")! as MayoParentElement)
-
- // the index of the start block in the mayo-document (y)
- let startBlockIndex = Array.from(this.document.children).indexOf(
- startBlockElement
- )
-
- let endBlockIndex = Array.from(this.document.children).indexOf(
- endBlockElement
- )
-
- // the index of the start element in the start block (x)
- let startElementIndex = 0
- if (startElement.inline) {
- startElementIndex = startBlockElement.interestingChildren.indexOf(
- startElement
- )
- } else if (startElement.block) {
- startElementIndex = startBlockElement.interestingChildren.indexOf(
- range.startContainer
- )
- }
-
- let endElementIndex = 0
- if (endElement.inline) {
- endElementIndex = endBlockElement.interestingChildren.indexOf(endElement)
- } else if (endElement.block) {
- endElementIndex = endBlockElement.interestingChildren.indexOf(
- range.endContainer
- )
- }
- switch (event.inputType) {
- case "insertReplacementText":
- event.dataTransfer.items[0].getAsString(text => {
- startElement.selfInsertText(text, range)
- this.setAttribute("dirty", "true")
- this.update()
- })
- break
- case "insertText": {
- let caret: CaretInstruction | null = null
- if (range.startContainer == range.endContainer) {
- caret = startElement.selfInsertText(event.data || "", range)
- event.preventDefault()
- } else if (startBlockElement == endBlockElement) {
- caret = (startBlockElement as MayoParentElement).insertTextAsCommonAncestor(
- startElement,
- endElement,
- event.data || "",
- range
- )
- } else {
- // TODO this is a textInsert across blocks, sounds hard
- }
-
- this.setAttribute("dirty", "true")
- this.update()
- this.updateSelection(caret)
- break
- }
- case "insertLineBreak": {
- let index = startBlockElement.interestingChildren.indexOf(startElement)
- startBlockElement.node.children.splice(index + 1, 0, u("break"))
- this.setAttribute("dirty", "true")
- this.update()
- event.preventDefault()
- break
- }
- case "insertParagraph": {
- let id = shortId()
- if ("value" in startElement.node) {
- // TODO isLiteralElement typeguard
-
- if (isTextOrInlineCode(startElement)) {
- insertParagraph(this.ast, startElement.node, range.startOffset, id)
- }
- } else if (startElement == startBlockElement) {
- if (Array.isArray(startElement.node.children)) {
- insertParagraph(
- this.ast,
- startElement.node.children[startElementIndex],
- range.startOffset,
- id
- )
- }
- } else {
- let idx = startElement.interestingChildren.indexOf(
- range.startContainer
- )
- if (Array.isArray(startElement.node.children)) {
- insertParagraph(
- this.ast,
- startElement.node.children[idx],
- range.startOffset,
- id
- )
- }
- }
-
- this.setAttribute("dirty", "true")
- this.update()
- this.updateSelection({
- type: "id" as const,
- id,
- index: 0,
- startOffset: 0,
- })
- break
- }
- case "deleteContentBackward": {
- let atStartOfSomeChild =
- range.startContainer == range.endContainer &&
- "node" in range.startContainer
- let child = range.startContainer as MayoElement
-
- let index = startElement.interestingChildren.indexOf(child)
-
- if (atStartOfSomeChild) {
- let somethingHappened = false
- switch (child.node.type) {
- case "strong":
- case "emphasis":
- case "delete":
- if (Array.isArray(startElement.node.children)) {
- startElement.node.children.splice(
- index,
- 1,
- ...child.node.children
- )
- }
- somethingHappened = true
- break
- case "inlineCode":
- // @ts-ignore
- child.node.type = "text"
- somethingHappened = true
- break
- }
- if (somethingHappened) {
- this.update()
- event.preventDefault()
- }
- this.updateSelection({
- type: "parent",
- parent: startBlockElement as MayoParentElement,
- index: index,
- startOffset: 0,
- })
- } else if (range.startContainer == range.endContainer) {
- let caret = startElement.selfDeleteContentBackward(range)
- this.setAttribute("dirty", "true")
- this.update()
- if (caret) {
- this.updateSelection(caret)
- }
- }
- break
- }
- case "deleteContentForward": {
- let atStartOfBlock =
- startElement == startBlockElement && range.collapsed
- }
- case "deleteByCut":
- case "insertOrderedList":
- case "insertUnorderedList":
- case "insertHorizontalRule":
- case "insertFromYank":
- case "insertFromDrop":
- case "insertFromPaste":
- case "insertFromPasteAsQuotation":
- case "insertTranspose":
- case "insertCompositionText":
- case "insertLink":
- case "deleteWordBackward":
- case "deleteWordForward":
- case "deleteSoftLineBackward":
- case "deleteSoftLineForward":
- case "deleteEntireSoftLine":
- case "deleteHardLineBackward":
- case "deleteHardLineForward":
- case "deleteByDrag":
- case "deleteContent":
- case "historyUndo":
- case "historyRedo":
- case "formatBold":
- case "formatItalic":
- case "formatUnderline":
- case "formatStrikeThrough":
- case "formatSuperscript":
- case "formatSubscript":
- case "formatJustifyFull":
- case "formatJustifyCenter":
- case "formatJustifyRight":
- case "formatJustifyLeft":
- case "formatIndent":
- case "formatOutdent":
- case "formatRemove":
- case "formatSetBlockTextDirection":
- case "formatSetInlineTextDirection":
- case "formatBackColor":
- case "formatFontColor":
- case "formatFontName":
- console.log(`unhandled input event: ${event.inputType}`)
- }
-
+ let transformOptions = await getTransformOptions(event)
+ this.node = produce(this.node, draft => transform(draft, transformOptions))
+ this.setAttribute("dirty", "true")
+ await this.requestUpdate()
+ // await this.updateComplete
+ // this.placeCaret()
event.preventDefault()
+ return
+
+ // let startContainerElement = startElement.container
+ // ? startElement
+ // : (startElement.closest("[container]")! as MayoParentElement)
+
+ // let startBlockElement = startElement.block
+ // ? startElement
+ // : (startElement.closest("[block]")! as MayoParentElement)
+
+ // let endBlockElement = endElement.block
+ // ? endElement
+ // : (endElement.closest("[block]")! as MayoParentElement)
+
+ // // the index of the start element in the start block (x)
+ // let startElementIndex = 0
+ // if (startElement.inline) {
+ // startElementIndex = startBlockElement.interestingChildren.indexOf(
+ // startElement
+ // )
+ // } else if (startElement.block) {
+ // startElementIndex = startBlockElement.interestingChildren.indexOf(
+ // range.startContainer
+ // )
+ // }
+
+ // let endElementIndex = 0
+ // if (endElement.inline) {
+ // endElementIndex = endBlockElement.interestingChildren.indexOf(endElement)
+ // } else if (endElement.block) {
+ // endElementIndex = endBlockElement.interestingChildren.indexOf(
+ // range.endContainer
+ // )
+ // }
+ // switch (event.inputType) {
+ // case "insertReplacementText":
+ // event.dataTransfer.items[0].getAsString(text => {
+ // startElement.selfInsertText(text, range)
+ // this.setAttribute("dirty", "true")
+ // })
+ // break
+ // case "insertText": {
+ // let caret: CaretInstruction | null = null
+ // if (range.startContainer == range.endContainer) {
+ // caret = startElement.selfInsertText(event.data || "", range)
+ // event.preventDefault()
+ // } else if (startBlockElement == endBlockElement) {
+ // caret = (startBlockElement as MayoParentElement).insertTextAsCommonAncestor(
+ // startElement,
+ // endElement,
+ // event.data || "",
+ // range
+ // )
+ // } else {
+ // // TODO this is a textInsert across blocks, sounds hard
+ // }
+
+ // this.setAttribute("dirty", "true")
+
+ // this.updateSelection(caret)
+ // break
+ // }
+ // case "insertLineBreak": {
+ // let index = startBlockElement.interestingChildren.indexOf(startElement)
+ // startBlockElement.node.children.splice(index + 1, 0, u("break"))
+ // this.setAttribute("dirty", "true")
+
+ // event.preventDefault()
+ // break
+ // }
+ // case "insertParagraph": {
+ // let id = shortId()
+ // if ("value" in startElement.node) {
+ // if (isTextOrInlineCode(startElement)) {
+ // insertParagraph(
+ // this.node,
+ // startElement.node,
+ // range.startOffset,
+ // id
+ // )
+ // }
+ // } else if (startElement == startBlockElement) {
+ // if (Array.isArray(startElement.node.children)) {
+ // insertParagraph(
+ // this.node,
+ // startElement.node.children[startElementIndex],
+ // range.startOffset,
+ // id
+ // )
+ // }
+ // } else {
+ // let idx = startElement.interestingChildren.indexOf(
+ // range.startContainer
+ // )
+ // if (Array.isArray(startElement.node.children)) {
+ // insertParagraph(
+ // this.node,
+ // startElement.node.children[idx],
+ // range.startOffset,
+ // id
+ // )
+ // }
+ // }
+
+ // this.setAttribute("dirty", "true")
+
+ // this.updateSelection({
+ // type: "id" as const,
+ // elementId: id,
+ // textChildIndex: 0,
+ // startOffset: 0,
+ // })
+ // break
+ // }
+ // case "deleteContentBackward": {
+ // if (startElement.node.type == "paragraph") {
+ // startElement = startElement as MayoParagraphElement
+
+ // if (startElement.atBeginningOfBlock(range)) {
+ // backspaceParagraph(this.node, startElement.node)
+ // }
+ // } else if (startElement != startBlockElement) {
+ // if (is.container(startElement.node.type)) {
+ // // go through the children and move the top-level text nodes out into the parent
+ // let indexOfTextNode = startElement.interestingChildren.indexOf(
+ // range.startContainer
+ // )
+ // if (Array.isArray(startElement.node.children)) {
+ // let textNode = startElement.node.children[indexOfTextNode]
+ // }
+ // } else if (typeof startElement.node.value == "string") {
+ // if (range.startOffset == startElement.node.value.length - 1) {
+ // startElement.node.type = "text"
+
+ // // TODO fix caret position
+ // // this.updateSelection()
+ // event.preventDefault()
+ // }
+ // }
+ // }
+
+ // let caret = startElement.selfDeleteContentBackward(range)
+
+ // this.setAttribute("dirty", "true")
+
+ // if (caret) {
+ // this.updateSelection(caret)
+ // }
+ // break
+ // }
+ // }
+
+ // event.preventDefault()
//this.updateForTransform(event)
}
updateForTransform(caret: CaretInstruction) {
this.setAttribute("dirty", "true")
- this.update()
this.updateSelection(caret)
}
@@ -327,51 +271,46 @@ export default class MayoDocumentElement extends HTMLElement {
}
save() {
- this.contents = toMarkdown(compact(this.ast))
+ this.contents = toMarkdown(compact(this.node))
this.dispatchEvent(new CustomEvent("save"))
}
- update() {
- if (!this.ast) {
- return
- }
- this.ast = compact(this.ast)
- render(convertToHtml(this.ast, this.ast), this.document)
+ render() {
+ return html`
+ ${this.node
+ ? this.node.children.map((child, index) => {
+ return html``
+ })
+ : ""}
+ `
}
- attributeChangedCallback(name: string, oldValue: any, newValue: any): void {
- if (name == "contents") {
- let contents = newValue
-
- this.ast = parse(contents)
-
- if (this.ast.children.length == 0) {
- this.ast.children.splice(0, 0, u("paragraph", [u("text", " ")]))
- }
- this.update()
- }
+ constructor() {
+ super()
+ this.addEventListener("keydown", this.handleKeydown)
+ this.addEventListener("beforeinput", this.handleInput)
}
connectedCallback() {
- render(
- html``,
- this
- )
- if (this.contents) {
- let contents = this.contents
- this.ast = parse(contents)
-
- if (this.ast.children.length == 0) {
- this.ast.children.splice(0, 0, u("paragraph", [u("text", " ")]))
- }
- this.update()
- }
+ super.connectedCallback()
}
}
diff --git a/src/elements/mayo-node.ts b/src/elements/mayo-node.ts
new file mode 100644
index 0000000..89378fd
--- /dev/null
+++ b/src/elements/mayo-node.ts
@@ -0,0 +1,199 @@
+import {customElement, LitElement, property} from "lit-element"
+import {html, render, TemplateResult} from "lit-html"
+import * as md from "mdast"
+import * as unist from "unist"
+import * as is from "../ast/is"
+import {spread} from "@open-wc/lit-helpers"
+import {spreadable} from "../ast/convert-to-html"
+import {repeat} from "lit-html/directives/repeat"
+
+@customElement("mayo-node")
+class MayoNodeElement extends LitElement {
+ @property()
+ type: string
+ @property({type: Number})
+ index: number
+ @property({type: Object})
+ node: md.Content
+ @property({type: Number})
+ depth: number
+ @property({type: Boolean})
+ container: boolean
+ @property({type: Boolean})
+ empty: boolean
+ @property({type: Boolean})
+ inline: boolean
+ @property({type: Boolean})
+ leaf: boolean
+ @property({type: Boolean})
+ list: boolean
+ @property({type: Boolean})
+ block: boolean
+ @property()
+ value: string
+ @property()
+ caret: string
+ @property({type: Number})
+ caretoffset: number
+ @property()
+ path: string
+
+ get textNode(): Text {
+ return (function recurse(node: Node): Text {
+ for (let child of Array.from(node.childNodes)) {
+ if (child.nodeType == child.TEXT_NODE) {
+ return child as Text
+ } else if (child.nodeType == child.COMMENT_NODE) {
+ continue
+ } else {
+ return recurse(child)
+ }
+ }
+ })(this)
+ }
+
+ createRenderRoot(): this {
+ return this
+ }
+
+ renderChildren(): TemplateResult | TemplateResult[] {
+ if (!this.node || !Array.isArray(this.node.children)) {
+ return html``
+ }
+ return html`${this.node.children.map((child, index) => {
+ let caret = child.data?.caret?.type
+ let offset = child.data?.caret?.offset
+ return html``
+ })}`
+ }
+
+ render(): string | TemplateResult {
+ if (!this.node) return html``
+ if (this.node.type == "heading") {
+ switch (this.node.depth) {
+ case 1:
+ return html`${this.renderChildren()}
`
+ case 2:
+ return html`${this.renderChildren()}
`
+ case 3:
+ return html`${this.renderChildren()}
`
+ case 4:
+ return html`${this.renderChildren()}
`
+ case 5:
+ return html`${this.renderChildren()}
`
+ case 6:
+ return html`${this.renderChildren()}
`
+ }
+ } else if (this.node.type == "blockquote") {
+ return html`${this.renderChildren()}
`
+ } else if (this.node.type == "break") {
+ return html`
`
+ } else if (this.node.type == "code") {
+ return html`${this.node.value}`
+ } else if (this.node.type == "delete") {
+ return html`${this.renderChildren()}`
+ } else if (this.node.type == "emphasis") {
+ return html`${this.renderChildren()}`
+ // } else if (this.node.type == "footnoteReference") {
+ // } else if (this.node.type == "footnote") {
+ } else if (this.node.type == "html") {
+ let element = document.createElement("div")
+ element.innerHTML = this.node.value
+ return html`${element}`
+ } else if (this.node.type == "imageReference") {
+ return ""
+ } else if (this.node.type == "image") {
+ return html``
+ // } else if (this.node.type == "linkReference") {
+ } else if (this.node.type == "link") {
+ return html`${this.renderChildren()}`
+ } else if (this.node.type == "listItem") {
+ // TODO spread, todo-box w/ toggle event
+ return html`
+ ${this.node == null
+ ? ""
+ : html``}${this.renderChildren()}
+ `
+ } else if (this.node.type == "list") {
+ // TODO this.node.spread
+ if (this.node.ordered) {
+ // TODO this.node.start
+ return html`
+ ${this.renderChildren()}
+
`
+ } else {
+ return html`
+ ${this.renderChildren()}
+
`
+ }
+ } else if (this.node.type == "paragraph") {
+ return html`${this.renderChildren()}
`
+ } else if (this.node.type == "strong") {
+ return html`${this.renderChildren()}`
+ } else if (this.node.type == "table") {
+ // TODO table
+ return ""
+ } else if (this.node.type == "thematicBreak") {
+ return html`
`
+ } else if (this.node.type == "yaml") {
+ return ""
+ } else if (this.node.type == "definition") {
+ return ""
+ } else if (this.node.type == "footnoteDefinition") {
+ return ""
+ } else if (this.node.type == "inlineCode") {
+ return html`${this.node.value}
`
+ } else if (this.node.type == "text") {
+ return html`${this.node.value}`
+ }
+ }
+
+ updated(props: Map) {
+ if (this.caret) {
+ console.log(this.caret, this.caretoffset)
+ let selection = document.getSelection()
+ let range = document.createRange()
+
+ selection.removeAllRanges()
+ if (this.caret == "collapsed" || this.caret == "start") {
+ range.setStart(this.textNode, this.caretoffset)
+ }
+ if (this.caret == "collapsed" || this.caret == "end") {
+ range.setEnd(this.textNode, this.caretoffset)
+ }
+
+ selection.addRange(range)
+ }
+ }
+}
+
+export default MayoNodeElement
diff --git a/src/elements/mayo-sidebar-tree.ts b/src/elements/mayo-sidebar-tree.ts
index bc30e6a..af6ad0c 100644
--- a/src/elements/mayo-sidebar-tree.ts
+++ b/src/elements/mayo-sidebar-tree.ts
@@ -37,6 +37,10 @@ export default class MayoSidebarTreeElement extends LitElement {
.arrow {
color: black;
}
+
+ mayo-sidebar-file {
+ font-size: 0.8em;
+ }
`
}
diff --git a/src/elements/mayo-sidebar.ts b/src/elements/mayo-sidebar.ts
index 5c02c94..7ca2ea2 100644
--- a/src/elements/mayo-sidebar.ts
+++ b/src/elements/mayo-sidebar.ts
@@ -48,7 +48,7 @@ export default class MayoSidebarElement extends LitElement {
?root=${true}
?open=${false}
cwd=${this.cwd}
- name=${basename(this.cwd)}
+ name=${this.cwd ? basename(this.cwd) : ""}
>
`
diff --git a/src/get-transform-options.ts b/src/get-transform-options.ts
new file mode 100644
index 0000000..ea1b3fd
--- /dev/null
+++ b/src/get-transform-options.ts
@@ -0,0 +1,43 @@
+import type {TransformOptions} from "./ast/transform"
+import BeforeInputEvent from "./before-input-event"
+import MayoNodeElement from "./elements/mayo-node"
+import type * as md from "mdast"
+import {find} from "unist-utils-core"
+
+export default async function getTransformOptions(
+ event: BeforeInputEvent
+): Promise {
+ let data = event.data
+
+ if (event.inputType == "insertReplacementText") {
+ // i'm going to regret this
+ // instead i should be creating a placeholder element
+ data = await new Promise(yay =>
+ event.dataTransfer.items[0].getAsString(yay)
+ )
+ }
+
+ let range = event.getTargetRanges()[0]
+ let start = range.startContainer.parentElement as MayoNodeElement
+ let end = range.endContainer.parentElement as MayoNodeElement
+
+ if (start.tagName.toLowerCase() != "mayo-node") {
+ start = start.closest("mayo-node")
+ }
+ if (end.tagName.toLowerCase() != "mayo-node") {
+ end = end.closest("mayo-node")
+ }
+
+ return {
+ data,
+ start: {
+ path: start.path,
+ offset: range.startOffset,
+ },
+ end: {
+ path: end.path,
+ offset: range.endOffset,
+ },
+ inputType: event.inputType,
+ }
+}
diff --git a/src/index.html b/src/index.html
index 06fd274..eaf9201 100644
--- a/src/index.html
+++ b/src/index.html
@@ -1,6 +1,5 @@
mayonaise
-
diff --git a/src/mayo.ts b/src/mayo.ts
index 1e8f02c..1fc503a 100644
--- a/src/mayo.ts
+++ b/src/mayo.ts
@@ -1,6 +1 @@
-import {controller} from "@github/catalyst"
-import elements from "./elements"
-
-for (let element of elements) {
- controller(element)
-}
+import "./elements"
diff --git a/src/mayo/elements/markdown/mayo-element.ts b/src/mayo/elements/markdown/mayo-element.ts
deleted file mode 100644
index 4910630..0000000
--- a/src/mayo/elements/markdown/mayo-element.ts
+++ /dev/null
@@ -1,234 +0,0 @@
-import type * as md from "mdast"
-import type * as unist from "unist"
-import {split} from "../../utils"
-import {CaretInstruction, MayoContentElement} from ".."
-export abstract class MayoElement<
- AstNodeType extends unist.Node
-> extends HTMLElement {
- node: AstNodeType
- get value(): string | undefined {
- return this.getAttribute("value")
- }
-
- get type(): string | undefined {
- return this.getAttribute("type")
- }
-
- get block(): boolean {
- return this.hasAttribute("block")
- }
-
- get leaf(): boolean {
- return this.hasAttribute("leaf")
- }
-
- get container(): boolean {
- return this.hasAttribute("container")
- }
-
- get inline(): boolean {
- return this.hasAttribute("inline")
- }
-
- get interestingChildren(): Node[] {
- return Array.from(this.childNodes).filter(
- n => "node" in n || n.nodeName == "#text"
- )
- }
- /**
- * Insert text when the range start and end are both in a single element
- * @param text the text to insert
- * @param range the selected range, which may be of interest
- */
- abstract selfInsertText(text: string, range: StaticRange): CaretInstruction
-
- connectedCallback(): void {
- return void void 0
- }
-}
-
-export class MayoEmptyElement<
- AstNodeType extends
- | md.ImageReference
- | md.LinkReference
- | md.Link
- | md.Break
- | md.ThematicBreak
- | md.Image
-> extends MayoElement {
- selfDeleteContentBackward(r: StaticRange): void {
- throw new TypeError()
- }
- selfInsertText(_text: string, _range: StaticRange): CaretInstruction {
- throw new Error("empty elements")
- }
-}
-
-export class MayoLiteralElement<
- AstNodeType extends md.Literal
-> extends MayoElement {
- insertText(selection: Selection, event: InputEvent) {
- let special = event.data!.match(/[~`*_]/)
- if (special) {
- console.log(this.nodeValue, this.node.value)
- } else {
- this.node.value = selection.focusNode!.nodeValue || ""
- return true
- }
- }
-
- selfInsertText(text: string, range: StaticRange): CaretInstruction {
- this.node.value =
- this.node.value.slice(0, range.startOffset) +
- text +
- this.node.value.slice(range.endOffset)
- return {
- type: "text",
- element: this.interestingChildren[0] as Text,
- startOffset: range.startOffset + 1,
- }
- }
-
- selfDeleteContentBackward(range: StaticRange): CaretInstruction {
- this.node.value =
- this.node.value.slice(0, range.startOffset) +
- this.node.value.slice(range.endOffset)
- return {
- type: "text",
- element: this.interestingChildren[0] as Text,
- startOffset: range.startOffset,
- }
- }
-}
-
-export class MayoParentElement<
- AstNodeType extends md.Parent
-> extends MayoElement {
- atBeginningOfBlock(range: StaticRange): boolean {
- return (
- range.collapsed &&
- this.interestingChildren.indexOf(range.startContainer) == 0 &&
- range.startOffset == 0
- )
- }
-
- selfInsertText(text: string, range: StaticRange): CaretInstruction {
- // TODO if the character is special, and the range is !caret then wrap the selected area
- let targetTextNode = range.startContainer
- let targetIndex = this.interestingChildren.indexOf(targetTextNode)
- if (targetIndex == -1) {
- throw new Error(`${targetTextNode} is not a child of ${this}`)
- }
-
- let targetAstNode = this.node.children[targetIndex]
- if (targetAstNode.type != "text") {
- throw new Error(
- `expected the ast node to be of type text, got ${targetAstNode.type}`
- )
- }
- targetAstNode.value =
- targetAstNode.value.slice(0, range.startOffset) +
- text +
- targetAstNode.value.slice(range.endOffset)
-
- let caret = {
- type: "parent" as const,
- parent: this,
- index: targetIndex,
- startOffset: range.startOffset + 1,
- }
-
- return caret
- // TODO replace this with code that creates a code node when you press `
- // let ticks = targetAstNode.value.match(/(\s)`([^`]+)`(\s)/)
-
- // if (ticks) {
- // let tickIndex = targetAstNode.value.indexOf(ticks[0])
- // let prespace = ticks[1]
- // let tickContent = ticks[2]
-
- // let aftspace = ticks[3]
- // this.node.children.splice(
- // targetIndex,
- // 1,
- // u("text", targetAstNode.value.slice(0, tickIndex) + prespace),
- // u("inlineCode", {id: shortId()}, tickContent),
- // u(
- // "text",
- // aftspace + targetAstNode.value.slice(tickIndex + ticks[0].length)
- // )
- // )
- // } else {
- // return caret
- // }
- }
-
- insertTextAsCommonAncestor(
- startElement: MayoContentElement,
- endElement: MayoContentElement,
- text: string,
- range: StaticRange
- ): CaretInstruction {
- // This is tricky for me right now
- // but, what i want to to i think is
- // to move the startNode and endNode (which are both text nodes) up to being direct
- // children of `this`, removing the other nodes between start and end
- // and then merging them if they're the same type...
- // i think.
- // i think.
- // TODO write a
-
- let startVal = range.startContainer.nodeValue?.slice(0, range.startOffset)
- let endVal = range.endContainer.nodeValue?.slice(range.endOffset)
-
- let startChildIndex = startElement.interestingChildren.indexOf(
- range.startContainer
- )
- let endChildIndex = endElement.interestingChildren.indexOf(
- range.endContainer
- )
- let startIndex = this.interestingChildren.indexOf(startElement)
- let endIndex = this.interestingChildren.indexOf(endElement)
- let snode
- if (startChildIndex != -1 && Array.isArray(startElement.node.children)) {
- snode = startElement.node.children[startChildIndex]
- } else {
- snode = startElement.node
- }
-
- let child = startElement.firstChild
- if (child.nodeName == "#text") {
- let element = child as Text
- return {type: "text", element, startOffset: 0}
- }
- }
-
- insertParagraph(range: StaticRange) {}
-
- selfDeleteContentBackward(range: StaticRange) {
- let targetTextNode = range.startContainer
- let targetIndex = this.interestingChildren.indexOf(targetTextNode)
- if (targetIndex == -1) {
- throw new Error(`${targetTextNode} is not a child of ${this}`)
- }
-
- let targetAstNode = this.node.children[targetIndex]
- if (targetAstNode.type != "text") {
- throw new Error(
- `expected the ast node to be of type text, got ${targetAstNode.type}`
- )
- }
- targetAstNode.value =
- targetAstNode.value.slice(0, range.startOffset) +
- targetAstNode.value.slice(range.endOffset)
-
- let caret = {
- type: "parent" as const,
- parent: this,
- index: targetIndex,
- startOffset: range.startOffset,
- }
-
- return caret
- }
-}
diff --git a/src/styles.css b/src/styles.css
index 260fe77..6cae5a1 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -190,7 +190,7 @@ mayo-strong {
mayo-strong:hover::after {
content: "**";
} */
-mayo-blockquote {
+blockquote {
display: flex;
border-left: 5px solid #3399ff;
padding-left: 5px;
diff --git a/tests/backspace-paragraph.test.ts b/tests/backspace-paragraph.test.ts
new file mode 100644
index 0000000..f9679e2
--- /dev/null
+++ b/tests/backspace-paragraph.test.ts
@@ -0,0 +1,102 @@
+import backspaceParagraph from "../src/ast/backspace-paragraph"
+import parse from "./parse"
+import toMarkdown from "mdast-util-to-markdown"
+import {select, selectAll} from "unist-utils-core"
+import * as md from "mdast"
+
+describe("backspaceParagraph", () => {
+ test("removes a new paragraph at the start", () => {
+ let tree = parse(`hello
+
+there`)
+ let expected = parse("hellothere")
+ let p = selectAll("paragraph", tree)[1]
+
+ backspaceParagraph(tree, p)
+
+ expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
+ expect(tree).toEqual(expected)
+ })
+
+ // test("removes a new paragraph at the start (in a code)", () => {
+ // let tree = parse(`\`hello\`
+
+ // \`there\``)
+ // let expected = parse("`hellothere`")
+ // let p = selectAll("paragraph", tree)[1]
+ // backspaceParagraph(tree, p)
+
+ // expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
+ // expect(tree).toEqual(expected)
+ // })
+
+ // test("removes a new paragraph at the start (in a complex place)", () => {
+ // let tree = parse(
+ // "this is *story __all about how my life__ got flipped* turned up side down"
+ // )
+ // let expected = parse(
+ // `this is *story __all ab__*
+
+ // *__out how my life__ got flipped* turned up side down`
+ // )
+ // let p = select("paragraph", tree)
+ // let text = selectAll("text", p)[2]
+ // backspaceParagraph(tree, text, 6)
+
+ // expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
+ // expect(tree).toEqual(expected)
+ // })
+
+ // test("removes a new paragraph at the start (in a complex place in a code)", () => {
+ // let tree = parse(
+ // "this is *story __all about `how` my life__ got flipped* turned up side down"
+ // )
+ // let expected = parse(
+ // `this is *story __all about \`h\`__*
+
+ // *__\`ow\` my life__ got flipped* turned up side down`
+ // )
+ // let p = select("paragraph", tree)
+ // let inlineCode = select("inlineCode", p)
+ // backspaceParagraph(tree, inlineCode, 1)
+
+ // expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
+ // expect(tree).toEqual(expected)
+ // })
+
+ // test("removes a new paragraph, maintaining whole nodes", () => {
+ // let tree = parse("split __mein two__ please")
+ // let expected = parse(`split __me__
+
+ // __in two__ please`)
+ // let p = select("paragraph", tree)
+ // let text = selectAll("text", p)[1]
+
+ // backspaceParagraph(tree, text, 2)
+
+ // expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
+ // expect(tree).toEqual(expected)
+ // })
+
+ it("works in a heading", () => {
+ let tree = parse(`# well, well
+
+, well`)
+ let expected = parse("# well, well, well")
+ let p = select("paragraph", tree)
+ backspaceParagraph(tree, p)
+ expect(tree).toEqual(expected)
+ })
+
+ // it("works in a heading with special elements", () => {
+ // let tree = parse("# well, _well_, well, well")
+ // let expected = parse(`# we
+
+ // ll, _well_, well, well`)
+ // let h1 = select("heading", tree)
+ // let text = selectAll("text", h1)[0]
+ // backspaceParagraph(tree, text, 2)
+ // expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
+ // expect(tree).toEqual(expected)
+ // })
+})
diff --git a/tests/parse.ts b/tests/parse.ts
new file mode 100644
index 0000000..1c0f083
--- /dev/null
+++ b/tests/parse.ts
@@ -0,0 +1,7 @@
+import removePosition from "unist-util-remove-position"
+import fromMarkdown from "mdast-util-from-markdown"
+import {Root} from "mdast"
+
+export default function parse(string: string): Root {
+ return removePosition(fromMarkdown(string), true) as Root
+}
diff --git a/tests/transform/insert-paragraph.test.ts b/tests/transform/insert-paragraph.test.ts
new file mode 100644
index 0000000..d0d4e6d
--- /dev/null
+++ b/tests/transform/insert-paragraph.test.ts
@@ -0,0 +1,104 @@
+import insertParagraph from "../../src/ast/insert-paragraph"
+import parse from "../parse"
+import toMarkdown from "mdast-util-to-markdown"
+import {select, selectAll} from "unist-utils-core"
+import * as md from "mdast"
+
+describe("insertParagraph", () => {
+ test("inserts a new paragraph where the cursor is", () => {
+ let tree = parse("hellothere")
+ let expected = parse(`hello
+
+there`)
+ let p = select("paragraph", tree)
+ let text = select("text", p)
+ insertParagraph(tree, text, 5)
+
+ expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
+ expect(tree).toEqual(expected)
+ })
+
+ test("inserts a new paragraph where the cursor is (in a code)", () => {
+ let tree = parse("`hellothere`")
+ let expected = parse(`\`hello\`
+
+\`there\``)
+ let p = select("paragraph", tree)
+ let text = select("inlineCode", p)
+ insertParagraph(tree, text, 5)
+
+ expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
+ expect(tree).toEqual(expected)
+ })
+
+ test("inserts a new paragraph where the cursor is (in a complex place)", () => {
+ let tree = parse(
+ "this is *story __all about how my life__ got flipped* turned up side down"
+ )
+ let expected = parse(
+ `this is *story __all ab__*
+
+*__out how my life__ got flipped* turned up side down`
+ )
+ let p = select("paragraph", tree)
+ let text = selectAll("text", p)[2]
+ insertParagraph(tree, text, 6)
+
+ expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
+ expect(tree).toEqual(expected)
+ })
+
+ test("inserts a new paragraph where the cursor is (in a complex place in a code)", () => {
+ let tree = parse(
+ "this is *story __all about `how` my life__ got flipped* turned up side down"
+ )
+ let expected = parse(
+ `this is *story __all about \`h\`__*
+
+*__\`ow\` my life__ got flipped* turned up side down`
+ )
+ let p = select("paragraph", tree)
+ let inlineCode = select("inlineCode", p)
+ insertParagraph(tree, inlineCode, 1)
+
+ expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
+ expect(tree).toEqual(expected)
+ })
+
+ test("inserts a new paragraph, maintaining whole nodes", () => {
+ let tree = parse("split __mein two__ please")
+ let expected = parse(`split __me__
+
+__in two__ please`)
+ let p = select("paragraph", tree)
+ let text = selectAll("text", p)[1]
+
+ insertParagraph(tree, text, 2)
+
+ expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
+ expect(tree).toEqual(expected)
+ })
+
+ it("works in a heading", () => {
+ let tree = parse("# well, well, well")
+ let expected = parse(`# well, well
+
+, well`)
+ let h1 = select("heading", tree)
+ let text = select("text", h1)
+ insertParagraph(tree, text, 10)
+ expect(tree).toEqual(expected)
+ })
+
+ it("works in a heading with special elements", () => {
+ let tree = parse("# well, _well_, well, well")
+ let expected = parse(`# we
+
+ll, _well_, well, well`)
+ let h1 = select("heading", tree)
+ let text = selectAll("text", h1)[0]
+ insertParagraph(tree, text, 2)
+ expect(toMarkdown(tree)).toEqual(toMarkdown(expected))
+ expect(tree).toEqual(expected)
+ })
+})
diff --git a/tests/transform/insert-text.test.ts b/tests/transform/insert-text.test.ts
new file mode 100644
index 0000000..429d9f1
--- /dev/null
+++ b/tests/transform/insert-text.test.ts
@@ -0,0 +1,100 @@
+import type {TransformHandler} from "../../src/ast/transform"
+import {visit} from "unist-utils-core"
+import insertText from "../../src/ast/transform/insert-text"
+import parse from "../parse"
+import toMarkdown from "mdast-util-to-markdown"
+import {select, selectAll} from "unist-utils-core"
+import * as md from "mdast"
+import * as unist from "unist"
+
+describe(insertText, () => {
+ it("inserts text in the correct place", () => {
+ let tree = parse("hello again")
+ let expected = parse("hello! again")
+ insertText(tree, {
+ data: "!",
+ inputType: "insertText",
+ start: {
+ node: select("text", tree),
+ offset: 5,
+ },
+ end: {
+ node: select("text", tree),
+ offset: 5,
+ },
+ })
+ expect(expected).toEqual(tree)
+ })
+
+ it("inserts text in the correct in an inline code", () => {
+ let tree = parse("hello `code` again")
+ let expected = parse("hello `co-de` again")
+ insertText(tree, {
+ data: "-",
+ inputType: "insertText",
+ start: {
+ node: select("inlineCode", tree),
+ offset: 2,
+ },
+ end: {
+ node: select("inlineCode", tree),
+ offset: 2,
+ },
+ })
+ expect(expected).toEqual(tree)
+ })
+
+ it("inserts text in a nested thing", () => {
+ let tree = parse("hello _**hey `wow` nice**_ again")
+ let expected = parse("hello _**hey `wow` niice**_ again")
+ insertText(tree, {
+ data: "i",
+ inputType: "insertText",
+ start: {
+ node: selectAll("text", tree)[2],
+ offset: 2,
+ },
+ end: {
+ node: selectAll("text", tree)[2],
+ offset: 2,
+ },
+ })
+ expect(expected).toEqual(tree)
+ })
+
+ it("inserts text across a range", () => {
+ let tree = parse("hello _**hey nice**_ again")
+ let expected = parse("hello _**hehe**_ again")
+ insertText(tree, {
+ data: "h",
+ inputType: "insertText",
+ start: {
+ node: selectAll("text", tree)[1],
+ offset: 2,
+ },
+ end: {
+ node: selectAll("text", tree)[1],
+ offset: 7,
+ },
+ })
+ expect(expected).toEqual(tree)
+ })
+
+ it("inserts text across a range as a common ancestor", () => {
+ let tree = parse("hello _**hey `code` nice**_ again")
+ let expected = parse("hello _**hehe**_ again")
+ insertText(tree, {
+ data: "-",
+ inputType: "insertText",
+ start: {
+ node: selectAll("text", tree)[1],
+ offset: 2,
+ },
+ end: {
+ node: selectAll("text", tree)[2],
+ offset: 2,
+ },
+ })
+ expect(expected).toEqual(tree)
+ })
+})
diff --git a/tests/transform/split.test.ts b/tests/transform/split.test.ts
new file mode 100644
index 0000000..1a55ccf
--- /dev/null
+++ b/tests/transform/split.test.ts
@@ -0,0 +1,93 @@
+import split from "../../src/ast/split"
+
+import parse from "../parse"
+import toMarkdown from "mdast-util-to-markdown"
+import {select, selectAll} from "unist-utils-core"
+import * as md from "mdast"
+
+describe("split", () => {
+ test("splits an ordinary text node", () => {
+ let tree = parse("hellothere")
+ let expected = [
+ parse(`hello`).children[0].children[0],
+ parse(`there`).children[0].children[0],
+ ]
+ let p = select("paragraph", tree)
+ let text = select("text", p)
+ let actual = split(tree, text, 5)
+
+ expect(toMarkdown(actual[0])).toEqual(toMarkdown(expected[0]))
+ expect(toMarkdown(actual[1])).toEqual(toMarkdown(expected[1]))
+ expect(actual).toEqual(expected)
+ })
+ test("splits a node in half", () => {
+ let tree = parse("_hellothere_")
+ let expected = [
+ parse(`_hello_`).children[0].children[0],
+ parse(`_there_`).children[0].children[0],
+ ]
+ let p = select("paragraph", tree)
+ let text = select("text", p)
+ let actual = split(tree, text, 5)
+ expect(actual).toEqual(expected)
+ })
+
+ it("works in a heading", () => {
+ let tree = parse("# well, well, well")
+ let expected = [
+ parse(`# well, well`).children[0].children[0],
+ parse(`, well`).children[0].children[0],
+ ]
+ let h1 = select("heading", tree)
+ let text = select("text", h1)
+ let actual = split(tree, text, 10)
+ expect(actual).toEqual(expected)
+ })
+
+ test("splits a complex node correct", () => {
+ let tree = parse("_oh **hey** ok_")
+ let expected = [
+ parse(`_oh **he**_`).children[0].children[0],
+ parse(`_**y** ok_`).children[0].children[0],
+ ]
+ let p = select("paragraph", tree)
+ let text = selectAll("text", p)[1]
+ let actual = split(tree, text, 2)
+
+ expect(toMarkdown(actual[0])).toEqual(toMarkdown(expected[0]))
+ expect(toMarkdown(actual[1])).toEqual(toMarkdown(expected[1]))
+ expect(actual).toEqual(expected)
+ })
+
+ test("splits a code node correct", () => {
+ let tree = parse("_oh **hello, `here` is it** ok_")
+ let expected = [
+ parse(`_oh **hello, \`he\`**_`).children[0].children[0],
+ parse(`_**\`re\` is it** ok_`).children[0].children[0],
+ ]
+ let p = select("paragraph", tree)
+ let text = select("inlineCode", p)
+
+ let actual = split(tree, text, 2)
+
+ expect(toMarkdown(actual[0])).toEqual(toMarkdown(expected[0]))
+ expect(toMarkdown(actual[1])).toEqual(toMarkdown(expected[1]))
+ expect(actual).toEqual(expected)
+ })
+
+ test("returns only the split node when there are other children", () => {
+ let tree = parse("one two **three** four")
+ let expected = [
+ parse(`**thr**`).children[0].children[0],
+ parse(`**ee**`).children[0].children[0],
+ ]
+ let p = select("paragraph", tree)
+ let text = selectAll("text", p)[1]
+
+ let actual = split(tree, text, 3)
+
+ expect(toMarkdown(actual[0])).toEqual(toMarkdown(expected[0]))
+ expect(toMarkdown(actual[1])).toEqual(toMarkdown(expected[1]))
+ expect(actual).toEqual(expected)
+ })
+})
diff --git a/tests/transform/unwrap.test.ts b/tests/transform/unwrap.test.ts
new file mode 100644
index 0000000..af3c373
--- /dev/null
+++ b/tests/transform/unwrap.test.ts
@@ -0,0 +1,10 @@
+import unwrap from "../src/ast/unwrap"
+
+import parse from "../parse"
+import toMarkdown from "mdast-util-to-markdown"
+import {select, selectAll} from "unist-utils-core"
+import * as md from "mdast"
+
+test("1=1", () => {
+ expect(1).toBe(1)
+})
diff --git a/tsconfig.json b/tsconfig.json
index 3039969..fa6c2e6 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -25,6 +25,8 @@
}
},
"include": [
- "src/**/*"
+ "src/**/*",
+ "tests/transform/insert-paragraph.test.ts",
+ "tests/transform/split.test.ts"
]
}
diff --git a/yarn.lock b/yarn.lock
index 686bc5a..33436f2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3052,6 +3052,14 @@ cli-spinners@^2.5.0:
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.5.0.tgz#12763e47251bf951cb75c201dfa58ff1bcb2d047"
integrity sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ==
+cli-truncate@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
+ integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==
+ dependencies:
+ slice-ansi "^3.0.0"
+ string-width "^4.2.0"
+
cli-width@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
@@ -3772,6 +3780,24 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+electron-context-menu@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/electron-context-menu/-/electron-context-menu-2.4.0.tgz#58e3a0e0ff847d3ce78ee81b49544a62b6bf0b00"
+ integrity sha512-6HRAEeFoMoBZyQ69FBGNQIVVDRBw8nYmvMPaV+CfRDa/spreHsjMD+XesJ/2/lMSAAMDTCgFCC24167Uer2cZw==
+ dependencies:
+ cli-truncate "^2.0.0"
+ electron-dl "^3.0.0"
+ electron-is-dev "^1.0.1"
+
+electron-dl@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/electron-dl/-/electron-dl-3.0.2.tgz#302a46f9a449ddce720cb8e7f2a24c386e19a26c"
+ integrity sha512-pRgE9Jbhoo5z6Vk3qi+vIrfpMDlCp2oB1UeR96SMnsfz073jj0AZGQwp69EdIcEvlUlwBSGyJK8Jt6OB6JLn+g==
+ dependencies:
+ ext-name "^5.0.0"
+ pupa "^2.0.1"
+ unused-filename "^2.1.0"
+
electron-installer-common@^0.10.2:
version "0.10.3"
resolved "https://registry.yarnpkg.com/electron-installer-common/-/electron-installer-common-0.10.3.tgz#40f9db644ca60eb28673d545b67ee0113aef4444"
@@ -3816,6 +3842,11 @@ electron-installer-redhat@^3.2.0:
word-wrap "^1.2.3"
yargs "^15.1.0"
+electron-is-dev@^1.0.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-1.2.0.tgz#2e5cea0a1b3ccf1c86f577cee77363ef55deb05e"
+ integrity sha512-R1oD5gMBPS7PVU8gJwH6CtT0e6VSoD0+SzSnYpNm+dBkcijgA+K7VAMHDfnRq/lkKPZArpzplTW6jfiMYosdzw==
+
electron-notarize@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/electron-notarize/-/electron-notarize-1.0.0.tgz#bc925b1ccc3f79e58e029e8c4706572b01a9fd8f"
@@ -4082,6 +4113,11 @@ escalade@^3.1.1:
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+escape-goat@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
+ integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==
+
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@@ -4399,6 +4435,21 @@ express@^4.17.1:
utils-merge "1.0.1"
vary "~1.1.2"
+ext-list@^2.0.0:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37"
+ integrity sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==
+ dependencies:
+ mime-db "^1.28.0"
+
+ext-name@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/ext-name/-/ext-name-5.0.0.tgz#70781981d183ee15d13993c8822045c506c8f0a6"
+ integrity sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==
+ dependencies:
+ ext-list "^2.0.0"
+ sort-keys-length "^1.0.0"
+
ext@^1.1.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
@@ -5414,6 +5465,11 @@ ignore@^5.1.4:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
+immer@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"
+ integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
+
import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@@ -5716,6 +5772,11 @@ is-number@^7.0.0:
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+is-plain-obj@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
+ integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
+
is-plain-obj@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
@@ -6900,7 +6961,7 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-mime-db@1.45.0:
+mime-db@1.45.0, mime-db@^1.28.0:
version "1.45.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea"
integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==
@@ -7032,6 +7093,11 @@ mkdirp@^1.0.3:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
+modify-filename@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/modify-filename/-/modify-filename-1.1.0.tgz#9a2dec83806fbb2d975f22beec859ca26b393aa1"
+ integrity sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=
+
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@@ -8156,6 +8222,13 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+pupa@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62"
+ integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==
+ dependencies:
+ escape-goat "^2.0.0"
+
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
@@ -8939,6 +9012,15 @@ slash@^3.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
+slice-ansi@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
+ integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==
+ dependencies:
+ ansi-styles "^4.0.0"
+ astral-regex "^2.0.0"
+ is-fullwidth-code-point "^3.0.0"
+
slice-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
@@ -8978,6 +9060,20 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
+sort-keys-length@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188"
+ integrity sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=
+ dependencies:
+ sort-keys "^1.0.0"
+
+sort-keys@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
+ integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0=
+ dependencies:
+ is-plain-obj "^1.0.0"
+
source-list-map@^2.0.0, source-list-map@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
@@ -10013,6 +10109,14 @@ untildify@^4.0.0:
resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
+unused-filename@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/unused-filename/-/unused-filename-2.1.0.tgz#33719c4e8d9644f32d2dec1bc8525c6aaeb4ba51"
+ integrity sha512-BMiNwJbuWmqCpAM1FqxCTD7lXF97AvfQC8Kr/DIeA6VtvhJaMDupZ82+inbjl5yVP44PcxOuCSxye1QMS0wZyg==
+ dependencies:
+ modify-filename "^1.1.0"
+ path-exists "^4.0.0"
+
upath@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"