Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import React from "react"
- import ReactDOMServer from "react-dom/server"
- import globals from "./globals"
- import "./NewNoteV02.css"
- function H1(props) {
- const x1 = "# ".length
- if (props.renderMarkup) {
- return (
- <h1 className={props.renderSemanticClasses ? "header-1" : null}>
- {props.renderIndents && "\n\t\t"}
- {props.children.slice(x1)}
- {props.renderIndents && "\n\t"}
- </h1>
- )
- }
- return (
- <h1 className="fw:700 c:gray-900">
- {!props.readOnly && <span className="c:gray-900">{props.children.slice(0, x1)}</span>}
- {props.children.slice(x1)}
- </h1>
- )
- }
- function H2(props) {
- const x1 = "## ".length
- if (props.renderMarkup) {
- return (
- <h2 className={props.renderSemanticClasses ? "header-2" : null}>
- {props.renderIndents && "\n\t\t"}
- {props.children.slice(x1)}
- {props.renderIndents && "\n\t"}
- </h2>
- )
- }
- return (
- <h2 className="fw:700 c:gray-900">
- {!props.readOnly && <span className="c:gray-800">{props.children.slice(0, x1)}</span>}
- {props.children.slice(x1)}
- </h2>
- )
- }
- function H3(props) {
- const x1 = "### ".length
- if (props.renderMarkup) {
- return (
- <h3 className={props.renderSemanticClasses ? "header-3" : null}>
- {props.renderIndents && "\n\t\t"}
- {props.children.slice(x1)}
- {props.renderIndents && "\n\t"}
- </h3>
- )
- }
- return (
- <h3 className="fw:700 c:gray-900">
- {!props.readOnly && <span className="c:gray-700">{props.children.slice(0, x1)}</span>}
- {props.children.slice(x1)}
- </h3>
- )
- }
- function H4(props) {
- const x1 = "#### ".length
- if (props.renderMarkup) {
- return (
- <h4 className={props.renderSemanticClasses ? "header-4" : null}>
- {props.renderIndents && "\n\t\t"}
- {props.children.slice(x1)}
- {props.renderIndents && "\n\t"}
- </h4>
- )
- }
- return (
- <h4 className="fw:700 c:gray-900">
- {!props.readOnly && <span className="c:gray-600">{props.children.slice(0, x1)}</span>}
- {props.children.slice(x1)}
- </h4>
- )
- }
- function H5(props) {
- const x1 = "##### ".length
- if (props.renderMarkup) {
- return (
- <h5 className={props.renderSemanticClasses ? "header-5" : null}>
- {props.renderIndents && "\n\t\t"}
- {props.children.slice(x1)}
- {props.renderIndents && "\n\t"}
- </h5>
- )
- }
- return (
- <h5 className="fw:700 c:gray-900">
- {!props.readOnly && <span className="c:gray-500">{props.children.slice(0, x1)}</span>}
- {props.children.slice(x1)}
- </h5>
- )
- }
- function H6(props) {
- const x1 = "###### ".length
- if (props.renderMarkup) {
- return (
- <h6 className={props.renderSemanticClasses ? "header-6" : null}>
- {props.renderIndents && "\n\t\t"}
- {props.children.slice(x1)}
- {props.renderIndents && "\n\t"}
- </h6>
- )
- }
- return (
- <h6 className="fw:700 c:gray-900">
- {!props.readOnly && <span className="c:gray-400">{props.children.slice(0, x1)}</span>}
- {props.children.slice(x1)}
- </h6>
- )
- }
- function Comment(props) {
- const x1 = "//".length
- if (props.renderMarkup) {
- return null
- }
- return (
- <div>
- {props.children.split("\n").map((children, index) => (
- <p key={index} className="c:gray-400">
- <span>{children.slice(0, x1)}</span>
- {children.slice(x1)}
- </p>
- ))}
- </div>
- )
- }
- function Paragraph(props) {
- if (props.renderMarkup) {
- return (
- <p className={props.renderSemanticClasses ? "paragraph" : null}>
- {props.renderIndents && "\n\t\t"}
- {props.children || <br />}
- {props.renderIndents && "\n\t"}
- </p>
- )
- }
- return (
- <p className="c:gray-900">
- {props.children || <br />}
- </p>
- )
- }
- function Blockquote(props) {
- const x1 = "> ".length
- const multiline = props.children.split("\n")
- if (props.renderMarkup) {
- return (
- <blockquote className={props.renderSemanticClasses ? "blockquote" : null}>
- {props.renderIndents && "\n\t\t"}
- <ul>
- {multiline.map((children, index) => (
- <>
- {props.renderIndents && "\n\t\t\t"}
- <li key={index}>
- {props.renderIndents && "\n\t\t\t\t"}
- {children.slice(x1) || <br />}
- {props.renderIndents && "\n\t\t\t"}
- </li>
- </>
- ))}
- {props.renderIndents && "\n\t\t"}
- </ul>
- {props.renderIndents && "\n\t"}
- </blockquote>
- )
- }
- return (
- <blockquote className="p:1 br:0.1" style={{ background: "hsla(var(--blue-a200), 0.05)" }}>
- {multiline.map((children, index) => (
- <p key={index} className="c:blue-a200">
- {!props.readOnly && <span>{children.slice(0, x1)}</span>}
- {children.slice(x1) || <br />}
- </p>
- ))}
- </blockquote>
- )
- }
- function CodeBlock(props) {
- const x1 = "```".length
- const x2 = props.children.length - "```".length
- // If we’re rendering markup, we need to drop the leading
- // and trailing newlines.
- const multiline = !props.renderMarkup
- ? props.children.slice(x1, x2).split("\n")
- : props.children.slice(x1 + (props.children.charAt(x1) === "\n"), x2 - (props.children.charAt(x2 - 1) === "\n")).split("\n")
- if (props.renderMarkup) {
- return (
- <code className={props.renderSemanticClasses ? "code-block" : null}>
- {props.renderIndents && "\n\t\t"}
- <ul>
- {multiline.map((children, index) => (
- <>
- {props.renderIndents && "\n\t\t\t"}
- <li key={index}>
- {props.renderIndents && "\n\t\t\t\t"}
- {children || <br />}
- {props.renderIndents && "\n\t\t\t"}
- </li>
- </>
- ))}
- {props.renderIndents && "\n\t\t"}
- </ul>
- {props.renderIndents && "\n\t"}
- </code>
- )
- }
- return (
- // `block` is needed for `code`.
- <code className="p:1 block b:gray-100 br:0.1 overflow -x:scroll" style={{ font: "calc(19px * 0.75)/1.5 'Monaco', 'Roboto Mono'", whiteSpace: "pre" }}>
- {multiline.map((children, index) => (
- <p key={index} className="c:gray-900">
- {!index && (
- !props.readOnly && (
- <span>
- {props.children.slice(0, x1)}
- </span>
- )
- )}
- {children || ((index > 0 && index + 1 !== multiline.length) && <br />)}
- {index + 1 === multiline.length && (
- !props.readOnly && (
- <span>
- {props.children.slice(0, x1)}
- </span>
- )
- )}
- </p>
- ))}
- </code>
- )
- }
- function SectionBreak(props) {
- if (props.renderMarkup) {
- return <hr className={props.renderSemanticClasses ? "section-break" : null} />
- }
- if (props.readOnly) {
- return <hr style={{ border: "2px solid hsl(var(--gray-200))" }} />
- }
- return (
- <div className="relative -x -y">
- <div className="absolute -x -y no-pointer-events">
- <div className="flex -c -y:center h:max">
- <hr style={{ border: "2px solid hsl(var(--gray-200))" }} />
- </div>
- </div>
- <p style={{ color: "transparent" }}>
- {props.children}
- </p>
- </div>
- )
- }
- const ComponentMap = {
- H1,
- H2,
- H3,
- H4,
- H5,
- H6,
- Comment,
- Paragraph,
- Blockquote,
- CodeBlock,
- SectionBreak
- }
- function Lex(data) {
- const items = []
- for (let x2 = 0; x2 <= data.length; x2++) { // Use `let`.
- let x1 = x2 // Use `let`.
- let Component = "" // Use `let`.
- switch (true) {
- // Header.
- case (
- data.slice(x2, x2 + 2) === "# " ||
- data.slice(x2, x2 + 3) === "## " ||
- data.slice(x2, x2 + 4) === "### " ||
- data.slice(x2, x2 + 5) === "#### " ||
- data.slice(x2, x2 + 6) === "##### " ||
- data.slice(x2, x2 + 7) === "###### "
- ):
- Component = ["H1", "H2", "H3", "H4", "H5", "H6"][data.slice(x2).indexOf("# ")]
- while (x2 < data.length && data.charAt(x2) !== "\n") {
- x2++
- }
- break
- // Comment.
- case data.slice(x2, x2 + 2) === "//":
- Component = "Comment"
- while (x2 < data.length && data.charAt(x2) !== "\n") {
- x2++
- }
- break
- // Blockquote.
- case data.charAt(x2) === ">":
- Component = "Blockquote"
- while (x2 < data.length && (data.charAt(x2) !== "\n" || data.slice(x2, x2 + 2) === "\n>")) {
- x2++
- }
- break
- // Code block (strict).
- case data.slice(x2, x2 + 3) === "```" && (() => {
- const peekStart = data.slice(x2 + 3).indexOf("```")
- if (peekStart === -1) {
- return
- }
- // We need to use `x2 + ` to make `peekStart` an
- // absolute index.
- const peekEnd = x2 + 3 + peekStart + 3
- return !data.charAt(peekEnd) || data.charAt(peekEnd) === "\n"
- })():
- Component = "CodeBlock"
- x2 += 3
- while (x2 < data.length && data.slice(x2, x2 + 3) !== "```") {
- x2++
- }
- x2 += 3
- break
- // Section break (strict).
- case data.slice(x2, x2 + 3) === "---" && (!data.charAt(x2 + 3) || data.charAt(x2 + 3) === "\n"):
- Component = "SectionBreak"
- x2 += 3
- break
- // Paragraph.
- default:
- Component = "Paragraph"
- while (x2 < data.length && data.charAt(x2) !== "\n") {
- x2++
- }
- break
- }
- const children = data.slice(x1, x2)
- items.push({ Component, _Component: ComponentMap[Component], children })
- }
- return items
- }
- // `restoreSession` restores a local storage session.
- //
- // FIXME: Error handling?
- function restoreSession(key) {
- const localStorageState = JSON.parse(localStorage.getItem(key))
- const setLS = data => localStorage.setItem(key, JSON.stringify(data))
- return [localStorageState, setLS]
- }
- const KEY_CODE_SLASH = 191
- const KEY_CODE_TAB = 9
- function shouldObscure({ Component, children }) {
- const ok = (
- Component === "Comment" ||
- (Component === "Paragraph" && !children)
- )
- return ok
- }
- // `readOnlyComponents` obscures non read-only components.
- function readOnlyComponents({ readOnly, Components: ComponentsAsIs }) {
- const Components = [...ComponentsAsIs] // Don’t mutate `ComponentsAsIs`.
- if (readOnly) {
- // We need to iterate backwards because we’re using
- // `splice`. See djave.co.uk/blog/read/splice-doesnt-work-very-well-in-a-javascript-for-loop
- // for reference.
- for (let x = Components.length - 2; x >= 0; x--) { // Use `let`.
- if (shouldObscure(Components[x]) && shouldObscure(Components[x + 1])) {
- Components.splice(x, 1)
- }
- }
- const leading = Components[0]
- if (Components.length > 1 && shouldObscure(leading)) {
- Components.splice(0, 1)
- }
- const trailing = Components[Components.length - 1]
- if (Components.length > 1 && shouldObscure(trailing)) {
- Components.splice(Components.length - 1, 1)
- }
- }
- return Components
- }
- const [session, storeSession] = restoreSession(`codex-editor-alpha-${globals.version}`)
- /* eslint-disable no-useless-escape */
- const defaultValue = `// Version ${globals.version}.\n\n> Hello! 🖖\n> \n> Here’s the tl;dr: this is a prototype for what I’m building, an interactive, WYSIWYG editor designed from scratch for developers.\n> \n> Feel free to play to poke around. 😉\n>\n> And if you’re interested in learning how this editor works, read on to the second header where I explain more.\n\n# What this editor supports (so far)\n\n# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6\n\n// Comment 👻 — these are hidden when ‘Markdown’ is disabled.\n\nParagraph — empty paragraphs are hidden when ‘Markdown’ is disabled.\n\n> (Multiline blockquotes)\n>\n> To be, or not to be, that is the question.\n>\n> William Shakespeare.\n\n\`\`\`\n(Multiline code blocks)\n\npackage main\n\nimport "fmt"\n\nfunc main() {\n\tfmt.Println("hello, world!")\n}\n\`\`\`\n\nAnd section breaks:\n\n---\n\nI’m working on unordered lists, ordered lists, and inline elements. After that, this editor will transition from alpha to beta. 🤠\n\n---\n\n# What you’re looking at\n\nIn order to help me understand all of the moving parts for the WYSIWYG, enhanced Markdown editor I’m building, I decided to take a break from working on the WYSIWYG aspect and just focus on lexing.\n\nWhat is lexing?\n\nLexing is the process of searching for qualifiers and tokens from text streams; so in programming, a qualifier could be the opening quote of a string, e.g. \`"\`, and the token would be the string, e.g. \`"hello, world"\`. Because I’m parsing Markdown, a qualifier could be the opening syntax of a header e.g. \`#\` and the token would be the header in full, e.g. \`# What you’re looking at\`.\n\nSo this prototype does a couple of things: the left-hand side is an editable textarea and the right-hand side is a read-only view of the textarea’s text stream. You can view your input as React components, HTML, JSON, or JSON with the VDOM, that is, all of the metadata this editor uses under-the-hood.\n\n# Some discrete examples\n\nI’m building this editor in React for a couple of reasons, namely, because I tried to do it _without_ React and it was too hard. While it _can_ be done, I wouldn’t recommend it…\n\nFor example, here’s the \`Paragraph\` component this editor uses under-the-hood. If you look closely, you’ll notice the component outputs one of two representations: markup and HTML. Markup is what you see when you’re viewing the editor as ‘HTML’. In essence, markup is simplified HTML, with optional semantic classes and indents. The other representation is the rendered HTML in ‘Components’ mode.\n\n\`\`\`\nfunction Paragraph(props) {\n\tif (props.renderMarkup) {\n\t\treturn (\n\t\t\t<p className={props.renderSemanticClasses ? "paragraph" : null}>\n\t\t\t\t{props.renderIndents && "\\n\\t\\t"}\n\t\t\t\t\t{props.children || <br />}\n\t\t\t\t{props.renderIndents && "\\n\\t"}\n\t\t\t</p>\n\t\t)\n\t}\n\treturn (\n\t\t<p className="c:gray-900">\n\t\t\t{props.children || <br />}\n\t\t</p>\n\t)\n}\n\`\`\``
- const RenderModes = {
- Components: 0,
- HTML: 1,
- JSON: 2,
- JSONWithMetadata: 3
- }
- const initialState = (session && session.data && session) || {
- renderMode: RenderModes.Components,
- readOnly: false,
- renderSemanticClasses: true,
- renderIndents: true,
- data: defaultValue,
- pos1: 0,
- pos2: 0,
- Components: Lex(defaultValue)
- }
- // Temporary fix: re-lex the localStorage session.
- ;(() => {
- if (!session || !session.data) {
- return
- }
- initialState.Components = Lex(session.data)
- })()
- function NewNoteV02(props) {
- const ref = React.useRef(null)
- const [editorState, setEditorState] = React.useState(initialState)
- React.useEffect(() => {
- const handleKeyDown = e => {
- if ((globals.isAppleOS ? !e.metaKey : !e.ctrlKey) || e.keyCode !== KEY_CODE_SLASH) {
- return
- }
- setEditorState({ ...editorState, readOnly: !editorState.readOnly})
- }
- document.addEventListener("keydown", handleKeyDown)
- return () => {
- document.removeEventListener("keydown", handleKeyDown)
- }
- }, [editorState])
- React.useEffect(() => {
- storeSession(editorState)
- })
- React.useLayoutEffect(() => {
- ref.current.selectionStart = editorState.pos1
- ref.current.selectionEnd = editorState.pos2
- }, [editorState.pos1, editorState.pos2])
- const handleSelect = e => {
- const [pos1, pos2] = [ref.current.selectionStart, ref.current.selectionEnd]
- setEditorState({ ...editorState, pos1, pos2 })
- }
- const handleChange = e => {
- const data = e.target.value
- setEditorState({ ...editorState, data, Components: Lex(data) })
- }
- const handleKeyDown = e => {
- if (e.keyCode !== KEY_CODE_TAB) {
- return
- }
- if (e.shiftKey) {
- e.preventDefault()
- return
- }
- e.preventDefault()
- const data = editorState.data.slice(0, editorState.pos1) + "\t" + editorState.data.slice(editorState.pos2)
- const pos1 = editorState.pos1 + 1
- const pos2 = editorState.pos2 + 1
- setEditorState({ ...editorState, data, pos1, pos2, Components: Lex(data) })
- }
- // Properties are sorted alphabetically.
- const preformatted = { font: "calc(18px * 0.75)/1.5 'Monaco', 'Roboto Mono'", tabSize: 2, whiteSpace: "pre" }
- const formatted = { font: "18px/1.5 'BlinkMacSystemFont', system-ui, -apple-system, 'Roboto'", tabSize: 2 }
- return (
- <main>
- <textarea ref={ref} className="p-x:2 p-y:4 b:gray-100 overflow -y:scroll" style={{ ...preformatted, border: "none", outline: "none", resize: "none", whiteSpace: "normal" }} value={editorState.data} onSelect={handleSelect} onChange={handleChange} onKeyDown={handleKeyDown} spellCheck={true} />
- <div className="p-x:2 p-y:4 overflow -y:scroll">
- {editorState.renderMode === RenderModes.Components && (
- <article style={formatted}>
- {readOnlyComponents(editorState).map(({ _Component: Component, children }, index) => (
- <Component key={index} readOnly={editorState.readOnly}>
- {children}
- </Component>
- ))}
- </article>
- )}
- {editorState.renderMode === RenderModes.HTML && (
- <article style={preformatted}>
- <p>
- {ReactDOMServer.renderToStaticMarkup(
- <article>
- {readOnlyComponents(editorState).map(({ _Component: Component, children }, index) => (
- <>
- {/* React generates a unique key
- warning because strings are keyless. */}
- {Component !== Comment && "\n\t"}
- <Component
- key={index}
- renderMarkup
- readOnly={editorState.readOnly}
- renderSemanticClasses={editorState.renderSemanticClasses}
- renderIndents={editorState.renderIndents}
- >
- {children}
- </Component>
- </>
- ))}
- {"\n"}
- </article>
- )}
- </p>
- </article>
- )}
- {(
- editorState.renderMode === RenderModes.JSON ||
- editorState.renderMode === RenderModes.JSONWithMetadata
- ) && (
- <article style={preformatted}>
- <p>
- {JSON.stringify(
- (editorState.renderMode === RenderModes.JSON && { Components: readOnlyComponents(editorState) }) ||
- (editorState.renderMode === RenderModes.JSONWithMetadata && editorState),
- "", "\t"
- )}
- </p>
- </article>
- )}
- </div>
- <footer className="fixed -x -b" style={{ font: "16px/1 'BlinkMacSystemFont', system-ui, -apple-system, 'Roboto'" }}>
- <div className="p-x:1 flex -r -x:between b:white" style={{ borderTop: "1px solid hsl(var(--gray-200))" }}>
- <div className="flex -r">
- {generateSettings(editorState, setEditorState).slice(0, 4).map(props => (
- <label htmlFor={props.id}>
- <div className="p-x:0.4 p-y:1 flex -r -y:center pointer-events">
- <input id={props.id} type="radio" checked={props.checked} onChange={props.onChange} />
- <div className="w:0.4" />
- <p className="c:gray-900">
- {props.children}
- </p>
- </div>
- </label>
- ))}
- </div>
- <div className="flex -r">
- {generateSettings(editorState, setEditorState).slice(4, 7).map(props => (
- <label htmlFor={props.id}>
- <div className="p-x:0.4 p-y:1 flex -r -y:center pointer-events">
- <input id={props.id} type="checkbox" checked={props.checked} onChange={props.onChange} />
- <div className="w:0.4" />
- <p className="c:gray-900">
- {props.children}
- </p>
- </div>
- </label>
- ))}
- </div>
- </div>
- </footer>
- </main>
- )
- }
- const generateSettings = (editorState, setEditorState) => [
- { id: "render-components", checked: editorState.renderMode === RenderModes.Components, onChange: e => setEditorState({ ...editorState, renderMode: RenderModes.Components }), children: "Components" },
- { id: "render-html", checked: editorState.renderMode === RenderModes.HTML, onChange: e => setEditorState({ ...editorState, renderMode: RenderModes.HTML }), children: "HTML" },
- { id: "render-json", checked: editorState.renderMode === RenderModes.JSON, onChange: e => setEditorState({ ...editorState, renderMode: RenderModes.JSON }), children: "JSON" },
- { id: "render-json-with-metadata", checked: editorState.renderMode === RenderModes.JSONWithMetadata, onChange: e => setEditorState({ ...editorState, renderMode: RenderModes.JSONWithMetadata }), children: "JSON (with metadata)" },
- { id: "render-read-only", checked: !editorState.readOnly, onChange: e => setEditorState({ ...editorState, readOnly: !editorState.readOnly }), children: "Markdown (Components)" },
- { id: "render-semantic-classes", checked: editorState.renderSemanticClasses, onChange: e => setEditorState({ ...editorState, renderSemanticClasses: !editorState.renderSemanticClasses }), children: "Semantic classes (HTML)" },
- { id: "render-indents", checked: editorState.renderIndents, onChange: e => setEditorState({ ...editorState, renderIndents: !editorState.renderIndents }), children: "Indents (HTML)" }
- ]
- export default NewNoteV02
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement