Skip to content

Instantly share code, notes, and snippets.

@zaydek-old
Last active September 3, 2019 16:00
Show Gist options
  • Save zaydek-old/1d533dcaa833c84870b5080503374794 to your computer and use it in GitHub Desktop.
Save zaydek-old/1d533dcaa833c84870b5080503374794 to your computer and use it in GitHub Desktop.
Alpha development for v0.2 of the Codex editor opencodex.dev
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