Last active
September 3, 2019 16:00
-
-
Save zaydek-old/1d533dcaa833c84870b5080503374794 to your computer and use it in GitHub Desktop.
Alpha development for v0.2 of the Codex editor opencodex.dev
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from "react" | |
import globals from "./globals" | |
import "./NewNoteV02.css" | |
/* | |
* LexerComponents.js | |
*/ | |
const H1 = props => <h1 className="fw:700 c:gray-900">{!props.readOnly && <span className="c:gray-900"># </span>}{props.children.slice("# ".length)}</h1> | |
const H2 = props => <h2 className="fw:700 c:gray-900">{!props.readOnly && <span className="c:gray-800">## </span>}{props.children.slice("## ".length)}</h2> | |
const H3 = props => <h3 className="fw:700 c:gray-900">{!props.readOnly && <span className="c:gray-700">### </span>}{props.children.slice("### ".length)}</h3> | |
const H4 = props => <h4 className="fw:700 c:gray-900">{!props.readOnly && <span className="c:gray-600">#### </span>}{props.children.slice("#### ".length)}</h4> | |
const H5 = props => <h5 className="fw:700 c:gray-900">{!props.readOnly && <span className="c:gray-500">##### </span>}{props.children.slice("##### ".length)}</h5> | |
const H6 = props => <h6 className="fw:700 c:gray-900">{!props.readOnly && <span className="c:gray-400">###### </span>}{props.children.slice("###### ".length)}</h6> | |
function Comment(props) { | |
return ( | |
<div> | |
{props.children.split("\n").map((children, index) => ( | |
<p key={index} className="c:gray-400"> | |
<span> | |
{children.slice(0, "//".length)} | |
</span> | |
{children.slice("//".length)} | |
</p> | |
))} | |
</div> | |
) | |
} | |
function Paragraph(props) { | |
return ( | |
<p className="c:gray-900"> | |
{!props.children | |
? <span><br /></span> | |
: props.children | |
} | |
</p> | |
) | |
} | |
function Blockquote(props) { | |
return ( | |
<blockquote className="p-x:2 p-y:1 br:0.4" style={{ background: "hsla(var(--blue-a200), 0.05)" }}> | |
{props.children.split("\n").map((children, index) => ( | |
<p key={index} className="c:blue-a200"> | |
{!props.readOnly && ( | |
<span> | |
{children.slice(0, ">".length)} | |
</span> | |
)} | |
{!children.slice(">".length) | |
? <span><br /></span> | |
: children.slice(">".length) | |
} | |
</p> | |
))} | |
</blockquote> | |
) | |
} | |
function CodeBlock(props) { | |
const x1 = props.children.indexOf("\n") | |
const x2 = props.children.indexOf("\n```") | |
return ( | |
<code className="p-x:2 p-y:1 block b:gray-100 br:0.4 overflow -x:scroll" style={{ font: "calc(19px * 0.75)/1.5 'Monaco', 'Roboto Mono'", whiteSpace: "pre" }}> | |
{!props.readOnly && ( | |
<p> | |
<span> | |
{props.children.slice(0, x1)} | |
</span> | |
</p> | |
)} | |
{props.children.length > "```\n```".length && ( | |
props.children.slice(x1 + 1, x2).split("\n").map((children, index) => ( | |
<p key={index} className="c:gray-900"> | |
{!children | |
? <span><br /></span> | |
: children | |
} | |
</p> | |
)) | |
)} | |
{!props.readOnly && ( | |
<p> | |
<span> | |
{props.children.slice(x2 + 1)} | |
</span> | |
</p> | |
)} | |
</code> | |
) | |
} | |
function SectionBreak(props) { | |
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 | |
} | |
// /* eslint-disable no-unused-vars */ | |
// function debugStr(str) { | |
// return `^${str.split("·").join("").split("\u00a0").join("·").split("\n").join("\n")}$` | |
// } | |
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) === "###### " | |
): | |
// We need to remember `index` for `Component`. | |
const index = data.slice(x2).indexOf("# ") | |
for (; x2 < data.length && data.charAt(x2) !== "\n"; x2++) { | |
// No op. | |
} | |
Component = ["H1", "H2", "H3", "H4", "H5", "H6"][index] | |
break | |
// Comment. | |
case data.slice(x2, x2 + 2) === "//": | |
for (; (x2 < data.length && (data.charAt(x2) !== "\n" || data.slice(x2, x2 + 3) === "\n//")); x2++) { | |
// No op. | |
} | |
Component = "Comment" | |
break | |
// Blockquote. | |
case data.slice(x2, x2 + 1) === ">": | |
for (; x2 < data.length && (data.charAt(x2) !== "\n" || data.slice(x2, x2 + 2) === "\n>"); x2++) { | |
// No op. | |
} | |
Component = "Blockquote" | |
break | |
// Code Block. | |
case data.slice(x2, x2 + 3) === "```" && (() => { const end = x2 + 3 + data.slice(x2 + 3).indexOf("\n```"); return !data.charAt(end) || data.charAt(end) === "\n" })(): | |
for (; x2 < data.length && data.slice(x2, x2 + 4) !== "\n```"; x2++) { | |
// No op. | |
} | |
x2 += "\n```".length | |
Component = "CodeBlock" | |
break | |
// Section break. | |
case data.slice(x2, x2 + 3) === "---" && (!data.charAt(x2 + 3) || data.charAt(x2 + 3) === "\n"): | |
x2 += "---".length | |
Component = "SectionBreak" | |
break | |
// Paragraph. | |
default: | |
for (; x2 < data.length && data.charAt(x2) !== "\n"; x2++) { | |
// No op. | |
} | |
Component = "Paragraph" | |
break | |
} | |
const children = data.slice(x1, x2) | |
items.push({ Component, _Component: ComponentMap[Component], children }) | |
} | |
return items | |
} | |
/* | |
* NewNote.js | |
*/ | |
// `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_TAB = 9 | |
const KEY_CODE_SLASH = 191 | |
// `obscureReadOnly` returns whether a component should be | |
// obscured when the editor is read-only. | |
const shouldObscure = ({ Component, children }) => Component === "Comment" || (Component === "Paragraph" && !children) | |
function obscureReadOnlyComponents({ readOnly, Components }) { | |
const readOnlyComponents = [...Components] // Don’t mutate `Components`. | |
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. | |
// | |
// FIXME: Empty paragraph next to a comment next to a | |
// paragraph. | |
for (let x = readOnlyComponents.length - 1; x >= 0; x--) { // Use `let`. | |
while (true) { | |
const curr = readOnlyComponents[x] | |
const next = readOnlyComponents[x + 1] || null | |
if ((curr && shouldObscure(curr)) && (!x || (!next || (next && shouldObscure(next))))) { | |
readOnlyComponents.splice(x, 1) | |
continue | |
} | |
break | |
} | |
} | |
} | |
return readOnlyComponents | |
} | |
const [session, storeSession] = restoreSession("codex-editor-alpha-0.2.0") | |
// const defaultValue = "# Header\n## Header 2\n### Header 3\n#### Header 4\n##### Header 5\n###### Header 6\n\n// Comment.\n\nThis is a paragraph.\n\n> Blockquote.\n\n> Multiline blockquote.\n> Multiline blockquote.\n\n---\n\n# Section A\n\nThis is a section.\n\n---\n\n# Section B\n\nThis is a section." | |
const defaultValue = "> This is a prototype of the non-WYSIYWYG editor for 0.2. Prototypes like this inform me of all the ‘gotchas’ to look out for when I go about making it interactive.\n>\n> This version supports all headers, comments (like these!), paragraphs, blockquotes (now with multiline support), code blocks, 🎉 and section breaks (---).\n>\n> You can press cmd-/ on Mac or ctrl-/ on Windows and Linux to toggle visible read-only mode (disables markdown).\n\n# Your first program in Go!\n\n// Psst… this a comment — visible to you, invisible to everyone else. You can use them to annotate your note. They are obscured in read-only mode (cmd-/ on Mac and ctrl-/ on Windows and Linux).\n\nIn Go, the first program you’ll learn how to read, write, and compile is the quintessential “Hello, world!”. Here’s how to get started.\n\nIn your code editor of choice, type the following (don’t copy!):\n\n```\npackage main\n\nimport \"fmt\"\n\nfunc main() {\n\tfmt.Println(\"hello, world!\")\n}\n```\n\nThen in the command-line, we’ll compile our program like so:\n\n```\n$ go run main.go\nHello, world!\n```\n\nCongratulations! You’ve just read, written, and compiled your first program in Go. Huzzah! 🙌" | |
const initialState = { | |
readOnly: false, | |
data: (session && session.data) || defaultValue, | |
pos1: 0, | |
pos2: 0, | |
Components: Lex((session && session.data) || defaultValue) | |
} | |
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]) | |
// `handleSelect` updates the editor’s cursor. | |
const handleSelect = e => { | |
const [pos1, pos2] = [ref.current.selectionStart, ref.current.selectionEnd] | |
setEditorState({ ...editorState, pos1, pos2 }) | |
} | |
// `handleChange` updates the editor’s content. | |
const handleChange = e => { | |
const data = e.target.value | |
setEditorState({ ...editorState, data, Components: Lex(data) }) | |
} | |
const handleKeyDownTab = e => { | |
if (e.keyCode !== KEY_CODE_TAB) { | |
return | |
} | |
// FIXME: Tab with selection and untab (with or without | |
// a selection). | |
if (editorState.pos1 !== editorState.pos2 || e.shiftKey) { | |
e.preventDefault() | |
return | |
} | |
// Synthetic keyboard event. | |
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) }) | |
} | |
return ( | |
<main> | |
{/* Styles are sorted alphabetically. */} | |
<textarea ref={ref} className="p:2 b:gray-100 br:1" style={{ border: "none", font: "calc(19px * 0.75)/1.5 'Monaco', 'Roboto Mono'", outline: "none", resize: "none", tabSize: 2 }} value={editorState.data} onSelect={handleSelect} onChange={handleChange} onKeyDown={handleKeyDownTab} spellCheck={true} /> | |
<article className="p:2 overflow -y:scroll br:1" style={{ font: "19px/1.5 'BlinkMacSystemFont', system-ui, -apple-system, 'Roboto'", overflowWrap: "break-word", tabSize: 2 }}> | |
{obscureReadOnlyComponents(editorState).map(({ _Component: Component, children }, index) => ( | |
<Component key={index} readOnly={editorState.readOnly}> | |
{children} | |
</Component> | |
))} | |
</article> | |
<footer className="p:2 b:gray-100 br:1 overflow -y:scroll"> | |
<p style={{ font: "calc(19px * 0.75)/1.5 'Monaco', 'Roboto Mono'", tabSize: 2, whiteSpace: "pre" }}> | |
{JSON.stringify(editorState, "\n", "\t")} | |
</p> | |
</footer> | |
</main> | |
) | |
} | |
export default NewNoteV02 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment