oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1// Generates the Yjs updates behind CARLOS Docs' sample documents
2// (internal/docs/seed.go). Same reasoning as generate.mjs: there is no
3// Go-side Yjs encoder, so seed content has to arrive as a real update
4// produced by real yjs, exactly like a person's own typing would
5// produce one — never a text column written directly.
6//
7// The .txt files under internal/docs/seeddata/ are the editable
8// source; this script is what turns an edited .txt back into the
9// matching .update.bin the app actually applies. Run it after editing
10// any seed .txt file, and commit both together — the .bin is the
11// artefact, the .txt is the reason it looks the way it does.
12//
13// As of the fragment task, the update built here is a Y.XmlFragment
14// on root "prosemirror" (the same root and shape
15// fixtures/generate-fragment.mjs uses for core/src/fragment.rs's own
16// wire fixtures — see that file's own comment), not a bare Y.Text —
17// the read view now renders these through the CRDT's fragment
18// renderer, and a Y.Text-only update reads back empty through that
19// path.
20import * as Y from 'yjs'
21import { readFileSync, writeFileSync, readdirSync } from 'node:fs'
22import { join, dirname, basename } from 'node:path'
23import { fileURLToPath } from 'node:url'
24
25const seedDir = join(
26 dirname(fileURLToPath(import.meta.url)),
27 '..',
28 'internal',
29 'docs',
30 'seeddata',
31)
32
33const names = readdirSync(seedDir)
34 .filter((f) => f.endsWith('.txt'))
35 .map((f) => basename(f, '.txt'))
36 .sort()
37
38// titles mirrors internal/docs/seed.go's seedDocs Title field, by Base
39// — kept in sync by hand, the same way this script's own Base names
40// already mirror seed.go's. blocksFor (below) uses it to decide
41// whether a document's opening block is a literal echo of its own
42// title (drop it) or real content that merely looks similar (keep
43// it). If the two ever drift, the failure is visible and harmless
44// either way: a document whose title changed here keeps a stray
45// opening paragraph until this map is updated, never the reverse
46// (silently eating real content it doesn't recognise as a title).
47const titles = {
48 'packing-list': 'Packing List — Thursday Site Visit',
49 'proposal': 'Proposal: Refitting the Elm Street Reading Room',
50 'meeting-notes': 'Notes — Tuesday Planning Sync',
51 'documentation': 'Quick Reference: Resetting the Site Printer',
52 'letter': 'Letter to the Harbourview Allotment Committee',
53 'history': 'A History of the Millbrook Waterworks',
54}
55
56// blocksFor turns a .txt source into the paragraph/heading blocks its
57// matching Y.XmlFragment gets built from: split on blank lines, then
58// classify each block. A block reads as a heading only when it looks
59// like one — short, a single line, no trailing sentence-ending
60// punctuation — and never for the very first block (i === 0): that
61// guard exists independently of the title check below, because a
62// short, unpunctuated opening line is not always a title echo —
63// letter.txt's own opening block, "Dear Members of the Harbourview
64// Allotment Committee,", fits the heading shape (52 chars, one line,
65// trailing comma) but is a real salutation, not a section break. This
66// is what gives "Early years" and its neighbours in the Millbrook
67// waterworks history their real shape, rather than leaving every
68// block a paragraph.
69//
70// title, when given, is compared against the *first* paragraph only
71// (trimmed, exact match) — fix round 1: an earlier version of this
72// function dropped every document's opening block unconditionally, on
73// the assumption that a seed .txt always opens by restating its own
74// title. That is true for five of the six seed documents but not
75// letter.txt, whose opening line is real content the unconditional
76// version would have silently deleted. Comparing against the actual
77// title, rather than guessing from shape or position, is what lets
78// this stay correct for both cases at once. The comparison runs after
79// classification (on the finished `blocks` array), not before it on
80// `paras`: dropping the first *paragraph* before classifying would
81// shift every remaining block's index down by one, and the i !== 0
82// guard above would then wrongly protect the new index-0 block (e.g.
83// history.txt's "Early years") from ever becoming a heading.
84function blocksFor(text, title) {
85 const paras = text
86 .split(/\n\s*\n/)
87 .map((s) => s.trim())
88 .filter(Boolean)
89 const blocks = paras.map((p, i) => {
90 const looksLikeHeading =
91 p.length < 60 && !p.includes('\n') && !/[.!?:]$/.test(p) && i !== 0
92 if (looksLikeHeading) {
93 const h = new Y.XmlElement('heading')
94 h.setAttribute('level', '2')
95 const t = new Y.XmlText()
96 t.insert(0, p)
97 h.insert(0, [t])
98 return h
99 }
100 const el = new Y.XmlElement('paragraph')
101 const t = new Y.XmlText()
102 t.insert(0, p)
103 el.insert(0, [t])
104 return el
105 })
106 if (blocks.length > 0 && title !== undefined && paras[0] === title.trim()) {
107 return blocks.slice(1)
108 }
109 return blocks
110}
111
112for (const name of names) {
113 const text = readFileSync(join(seedDir, `${name}.txt`), 'utf8')
114
115 const doc = new Y.Doc()
116 // Fixed clientID, same reasoning as generate.mjs: running this twice
117 // must produce byte-identical output. It only needs to differ from
118 // the *other* clients that ever touch this specific document, and a
119 // freshly seeded document has none yet — the app's own random
120 // per-document ClientID (internal/docs/store.go's Store.Create) is
121 // what a real edit would be attributed to.
122 doc.clientID = 1
123 const blocks = blocksFor(text, titles[name])
124 doc.getXmlFragment('prosemirror').insert(0, blocks)
125
126 const update = Y.encodeStateAsUpdate(doc)
127 writeFileSync(join(seedDir, `${name}.update.bin`), Buffer.from(update))
128 console.log(`${name}: ${text.length} chars -> ${blocks.length} blocks -> ${update.length} bytes`)
129}
130