From 50a6de1b09219f4ab8a8d045041602ffdea9ea3c Mon Sep 17 00:00:00 2001 From: Grant Forrest Date: Sun, 9 Jun 2024 15:50:59 -0400 Subject: [PATCH] [ma] improve parsing --- apps/marginalia/web/package.json | 2 +- .../web/src/components/usfm/nodes.tsx | 220 +++++++++++++++--- 2 files changed, 187 insertions(+), 35 deletions(-) diff --git a/apps/marginalia/web/package.json b/apps/marginalia/web/package.json index 3417549b..5b0d0081 100644 --- a/apps/marginalia/web/package.json +++ b/apps/marginalia/web/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "app-dev": "vite", + "app-dev": "vite --open --serve", "build": "vite build", "preview": "vite preview" }, diff --git a/apps/marginalia/web/src/components/usfm/nodes.tsx b/apps/marginalia/web/src/components/usfm/nodes.tsx index f921c1c9..c93ad324 100644 --- a/apps/marginalia/web/src/components/usfm/nodes.tsx +++ b/apps/marginalia/web/src/components/usfm/nodes.tsx @@ -1,4 +1,13 @@ -import { ReactNode, JSX } from 'react'; +import { + ReactNode, + JSX, + useState, + useId, + createContext, + useContext, + Fragment, + useMemo, +} from 'react'; import { Reference } from './Reference.jsx'; export interface Node { @@ -7,7 +16,7 @@ export interface Node { line: string, marker: string, ) => { consumed: string; rest: string }; - render?: (consumed: string, marker: string) => JSX.Element | null; + Component?: (props: { consumed: string; marker: string }) => JSX.Element; multiline?: boolean; } @@ -51,16 +60,23 @@ const PARAGRAPH_CONSUME = (line: string, marker: string) => { // otherwise, consume the whole line return { consumed: withoutMarker, rest: '' }; }; +// eats everything after it - only used for context-creation, +// should not be used for markers that render content +const EVERYTHING_CONSUME = (line: string, marker: string) => { + return { consumed: line, rest: '' }; +}; // invisible by default const DEFAULT_RENDER = () => null; -export function UsfmNode({ text }: { text: string }) { +function render(text: string, key: string) { let remaining = text; let content: ReactNode[] = []; let match: Node | undefined = undefined; let prevRemaining = remaining; + let i = 0; do { + i++; match = undefined; // reset const marker = MARKER.exec(remaining); @@ -69,7 +85,11 @@ export function UsfmNode({ text }: { text: string }) { if (marker.index > 0) { const leading = remaining.slice(0, marker.index); remaining = remaining.slice(marker.index); - content.push({leading}); + content.push( + + {leading} + , + ); } // find node for marker match = nodeMap.get(marker[1]) ?? DEFAULT_NODE; @@ -77,7 +97,7 @@ export function UsfmNode({ text }: { text: string }) { const consume = match.consume ?? (match.multiline ? PARAGRAPH_CONSUME : DEFAULT_CONSUME); - const render = match.render ?? DEFAULT_RENDER; + const Comp = match.Component ?? DEFAULT_RENDER; let matchAgainst; let leftovers; if (match.multiline) { @@ -89,7 +109,9 @@ export function UsfmNode({ text }: { text: string }) { leftovers = otherLines.join('\n'); } const { consumed, rest } = consume(matchAgainst, marker[1]); - content.push(render(consumed, marker[1])); + content.push( + , + ); remaining = rest + leftovers; if (prevRemaining === remaining) { @@ -103,40 +125,84 @@ export function UsfmNode({ text }: { text: string }) { prevRemaining = remaining; } else { // add any text after markers. - content.push({remaining}); + content.push( + + {remaining} + , + ); } } while (remaining.length > 0 && match !== undefined); - return <>{content}; + return content; +} + +export function UsfmNode({ text }: { text: string }) { + const id = useId(); + const content = useMemo(() => render(text, id), [text, id]); + return {content}; } const DEFAULT_NODE: Node = { markers: [], consume: DEFAULT_CONSUME, - render: (consumed, marker) => ( - + Component: ({ consumed, marker, ...props }) => ( + [\{marker}] {consumed} ), }; +const NodeContext = createContext<{ chapterWord: string }>({ + chapterWord: '', +}); + const chapter: Node = { markers: ['c'], - render: (consumed) => { + Component: ({ consumed, ...props }) => { const number = parseInt(consumed, 10); + const { chapterWord } = useContext(NodeContext); return ( - + + {chapterWord ? `${chapterWord} ` : ''} {number} ); }, }; +const chapterLabel: Node = { + markers: ['cl'], + consume: EVERYTHING_CONSUME, + Component: ({ consumed, ...props }) => { + const firstLine = consumed.split('\n')[0]; + return ( + + + + ); + }, +}; + const paragraph: Node = { - markers: ['p'], - render: (consumed) => { + // todo: differentiate types of paragraphs? + markers: ['p', 'pm', 'pmo', 'pmc', 'pmr'], + Component: ({ consumed, ...props }) => { return ( -
+
); @@ -146,14 +212,14 @@ const paragraph: Node = { const verse: Node = { markers: ['v'], - render: (consumed) => { + Component: ({ consumed, ...props }) => { const trimmed = consumed.trimStart(); // first thing should be a number const firstSpaceIndex = trimmed.indexOf(' '); const number = parseInt(trimmed.slice(0, firstSpaceIndex), 10); const rest = trimmed.slice(firstSpaceIndex + 1); return ( - + {number} @@ -163,14 +229,14 @@ const verse: Node = { const word: Node = { markers: ['w', '+w'], - render: (consumed) => { + Component: ({ consumed, ...props }) => { // remove strong's reference const [word, strongs, ...extras] = consumed.split('|'); if (extras.length > 0) { console.error('unexpected extra data in word: ' + extras.join('|')); } return ( - + {word.trim()} ); @@ -180,7 +246,7 @@ const word: Node = { const quote: Node = { markers: ['q', 'q1', 'q2', 'q3', 'q4', 'q5'], multiline: true, - render: (consumed, marker) => { + Component: ({ consumed, marker, ...props }) => { const level = parseInt(marker.slice(1), 10); return (
@@ -198,12 +265,12 @@ const quote: Node = { const crossReference: Node = { markers: ['x'], - render: (consumed) => { + Component: ({ consumed, ...props }) => { // parse caller const [caller, ...rest] = consumed.trim().split(' '); return ( - + ); @@ -212,31 +279,59 @@ const crossReference: Node = { const crossReferenceText: Node = { markers: ['xt'], - render: (consumed) => { - return {consumed}; + Component: ({ consumed, ...props }) => { + return ( + + {consumed} + + ); }, }; const majorTitle: Node = { markers: ['mt1', 'mt2', 'mt3'], - render: (consumed, marker) => { + Component: ({ consumed, marker, ...props }) => { if (marker === 'mt1') { - return

{consumed}

; + return ( +

+ {consumed} +

+ ); } else if (marker === 'mt2') { - return

{consumed}

; + return ( +

+ {consumed} +

+ ); } - return {consumed}; + return {consumed}; + }, +}; + +const majorSection: Node = { + markers: ['ms1', 'ms2', 'ms3', 'ms4', 'ms5', 'ms6'], + Component: ({ consumed, marker, ...props }) => { + const level = parseInt(marker.slice(2), 10); + return ( +

+ {consumed} +

+ ); }, }; const footnote: Node = { markers: ['f'], - render: (consumed) => { + Component: ({ consumed, ...props }) => { // parse caller const [caller, ...rest] = consumed.trim().split(' '); return ( - + ); @@ -245,22 +340,73 @@ const footnote: Node = { const footnoteText: Node = { markers: ['ft'], - render: (consumed) => { - return {consumed}; + Component: ({ consumed, ...props }) => { + return ( + + {consumed} + + ); }, }; const wordsOfJesus: Node = { markers: ['wj'], - render: (consumed) => { + Component: ({ consumed, ...props }) => { + return ( + + + + ); + }, +}; + +const closing: Node = { + markers: ['cls'], + multiline: true, + Component: ({ consumed, ...props }) => { + return ( + + + + ); + }, +}; + +const selah: Node = { + markers: ['qs'], + Component: ({ consumed, ...props }) => { return ( - + ); }, }; +const descriptive: Node = { + markers: ['d'], + Component: ({ consumed, ...props }) => { + return ( + + {consumed} + + ); + }, +}; + +const blankLine: Node = { + markers: ['b'], + Component: ({ consumed, marker, ...props }) =>
, +}; + // omitted const toc: Node = { markers: ['toc1', 'toc2', 'toc3'], @@ -317,6 +463,12 @@ const nodes = [ crossReferenceReference, crossReferenceText, wordsOfJesus, + closing, + chapterLabel, + majorSection, + selah, + descriptive, + blankLine, ]; const nodeMap = new Map();