From 14ffebd8b1bca7553d7e5ee15e31113e8c5b930f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 21:33:42 +0000 Subject: [PATCH 1/3] Grid script: explainer, online dictionary and converter New section /script/ (CN) and /script/en/ (EN): - the idea: parts, words, lines as graph edges, numbers, voices, with live drawings - dictionary: searchable in both languages, write any word to see how it draws and reads - converter: page source to drawing (SVG, text, link) and drawing back to source, with a grid editor lib/glyph.js ports glyph-cli 0.2.0 to JavaScript and uses its bundled vocabulary; npm test checks it against fixtures generated by the CLI itself. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01RH7A8tqLXxxGiks9mUWtyE --- .github/workflows/test.yml | 16 + README.md | 5 + app/script.css | 117 + app/script.js | 396 +++ kit/nav.cn.json | 10 + kit/nav.en.json | 10 + lib/glyph-vocab.json | 545 ++++ lib/glyph-zh.json | 90 + lib/glyph.js | 473 ++++ package.json | 1 + script/convert/en/index.html | 122 + script/convert/index.html | 122 + script/dictionary/en/index.html | 72 + script/dictionary/index.html | 72 + script/en/index.html | 184 ++ script/index.html | 184 ++ scripts/glyph_fixtures.py | 53 + tests/glyph-cli-fixtures.json | 3325 +++++++++++++++++++++++ tests/glyph-examples/conversation.txt | 8 + tests/glyph-examples/how-many.txt | 4 + tests/glyph-examples/pulsars.txt | 5 + tests/glyph-examples/question.txt | 3 + tests/glyph-examples/saw-that.txt | 5 + tests/glyph-examples/saw-your-world.txt | 2 + tests/glyph-examples/shadow.txt | 5 + tests/glyph-examples/third-voice.txt | 4 + tests/glyph.test.mjs | 134 + 27 files changed, 5967 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 app/script.css create mode 100644 app/script.js create mode 100644 lib/glyph-vocab.json create mode 100644 lib/glyph-zh.json create mode 100644 lib/glyph.js create mode 100644 package.json create mode 100644 script/convert/en/index.html create mode 100644 script/convert/index.html create mode 100644 script/dictionary/en/index.html create mode 100644 script/dictionary/index.html create mode 100644 script/en/index.html create mode 100644 script/index.html create mode 100644 scripts/glyph_fixtures.py create mode 100644 tests/glyph-cli-fixtures.json create mode 100644 tests/glyph-examples/conversation.txt create mode 100644 tests/glyph-examples/how-many.txt create mode 100644 tests/glyph-examples/pulsars.txt create mode 100644 tests/glyph-examples/question.txt create mode 100644 tests/glyph-examples/saw-that.txt create mode 100644 tests/glyph-examples/saw-your-world.txt create mode 100644 tests/glyph-examples/shadow.txt create mode 100644 tests/glyph-examples/third-voice.txt create mode 100644 tests/glyph.test.mjs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d04a4c7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,16 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + - run: npm test diff --git a/README.md b/README.md index adca7fd..1531680 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Static files, no build step, no server, zero running cost. Published with GitHub | --- | --- | | `index.html`, `en/index.html` | The entrance: the era at a glance, Chinese and English | | `archive/2219-signal/` | The full record of the 2219 signal from Ross 128 b: the log, arrival times, the beacon decoder, letters | +| `script/` | The grid script: how the translating machine writes down what Ross 128 b sends (`script/`), the online dictionary (`script/dictionary/`) and a converter between page source and drawings (`script/convert/`), Chinese with English under `en/`. `lib/glyph.js` is a JavaScript port of [glyph-cli](https://github.com/InterImm/glyph-cli) 0.2.0 and `lib/glyph-vocab.json` is its bundled vocabulary; `lib/glyph-zh.json` holds the Chinese glosses | | `pulsars/` | The pulsar ledger, live: the pulsars that keep the MC clock, the ledger nodes and the timing residuals where the beacon repeats. Self-contained (its own page, `app/pulsars.js` and `app/pulsars.css`) so it can move to its own repo later | | `kit/` | The phase 2 design kit, 137, published at https://interstellar.interimm.org/kit/ ([kit/README.md](kit/README.md)) | | `app/` | Page scripts and styles | @@ -17,6 +18,10 @@ Static files, no build step, no server, zero running cost. Published with GitHub Run it locally with any static server, for example `python3 -m http.server`, and open http://localhost:8000/. +`npm test` checks `lib/glyph.js` against what glyph-cli itself outputs (`tests/glyph-cli-fixtures.json`). After a new glyph-cli +release or a vocabulary change, copy its `vocab.json` to `lib/glyph-vocab.json`, add the Chinese glosses to `lib/glyph-zh.json`, +and regenerate the fixtures with `pip install glyph-cli` and `python3 scripts/glyph_fixtures.py`. + The previous Hugo site (2022, "飞向恒星 / we must leave the solar system") is in the git history. The draft about the first Earth–Mars conflict lives on the branch `cms/interstellar/草稿`. diff --git a/app/script.css b/app/script.css new file mode 100644 index 0000000..f683dee --- /dev/null +++ b/app/script.css @@ -0,0 +1,117 @@ +/* The grid script pages only. Everything shared is in kit/interimm.css. */ +.gs-hero h1 { font-size: var(--step-4); } +.gs-hero .lede { max-width: 46rem; } +.gs-tabs { margin-top: 1.75rem; background: var(--deep); } + +/* drawings: square pixels, never smoothed */ +.gs-draw svg { display: block; width: 100%; height: auto; image-rendering: pixelated; } +.gs-draw-part { width: 3.25rem; flex: none; } +.gs-draw-word { width: 6rem; flex: none; } +.gs-draw { border: 2px solid var(--border-strong); background: var(--bg); } + +.gs-intro { display: grid; gap: 1rem; max-width: 42rem; } +.gs-intro p { margin: 0; } +.gs-points { display: grid; gap: 0.6rem; margin: 0; padding-left: 1.25rem; } +.gs-points li::marker { color: var(--them); font-family: var(--font-mono); } + +/* the sixteen parts */ +.gs-parts { display: grid; gap: 0.5rem; grid-template-columns: repeat(auto-fill, minmax(min(100%, 13.5rem), 1fr)); list-style: none; margin: 0; padding: 0; } +.gs-part { display: flex; gap: 0.85rem; align-items: center; padding: 0.6rem; background: var(--surface); border: 2px solid var(--border); } +.gs-part > div:last-child { display: grid; gap: 0.1rem; min-width: 0; } +.gs-part code, .gs-word code, .gs-entry code { font-family: var(--font-mono); font-size: 0.75rem; letter-spacing: 0.06em; color: var(--them); } +.gs-part strong { font-size: 1rem; } +.gs-part span { font-size: 0.85rem; color: var(--muted); } +.gs-part span::before { content: "→ "; color: var(--us); } + +/* words */ +.gs-words { display: flex; flex-wrap: wrap; gap: 0.75rem; list-style: none; margin: 0; padding: 0; } +.gs-word { display: grid; gap: 0.3rem; justify-items: start; padding: 0.6rem; background: var(--surface); border: 2px solid var(--border); } +.gs-word span { font-size: 0.92rem; } + +/* worked examples: a drawing beside its source and the graph it reads */ +.gs-examples { display: grid; gap: 1.5rem; } +.gs-example { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1.25rem; align-items: start; margin: 0; padding: 1rem; background: var(--surface); border: 2px solid var(--border-strong); box-shadow: var(--shadow); } +.gs-example > .gs-draw { max-width: 30rem; } +.gs-example figcaption { display: grid; gap: 0.75rem; justify-items: start; min-width: 0; } +.gs-example h3 { margin: 0; font-size: 1.05rem; } +.gs-src, .gs-rows { margin: 0; padding: 0.6rem 0.75rem; max-width: 100%; overflow-x: auto; background: var(--deep); border: 2px solid var(--border); font-family: var(--font-mono); font-size: 0.82rem; line-height: 1.45; white-space: pre; color: var(--text); } +.gs-rows { line-height: 1.15; letter-spacing: 0.08em; } + +/* the graph a page reads as */ +.gs-graph { display: grid; gap: 0.6rem; margin: 0; padding: 0; list-style: none; counter-reset: edge; } +.gs-graph li { counter-increment: edge; display: grid; gap: 0.25rem; } +.gs-graph p { margin: 0; } +.gs-edge { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; } +.gs-edge::before { content: counter(edge) "."; font-family: var(--font-mono); font-size: 0.8rem; color: var(--muted); } +.gs-node { padding: 0.05rem 0.5rem; border: 2px solid var(--text); background: var(--bg); } +.gs-rel { font-family: var(--font-mono); font-size: 0.82rem; color: var(--muted); } +.gs-rel::before { content: "—"; } +.gs-rel::after { content: "→"; } +.gs-reply { display: flex; flex-wrap: wrap; gap: 0.25rem 0.75rem; padding-left: 1.4rem; font-size: 0.9rem; color: var(--muted); } +.gs-reply i { font-style: normal; font-family: var(--font-mono); font-size: 0.78rem; color: var(--text); } +.gs-sym { display: inline-grid; place-items: center; width: 1.4rem; height: 1.4rem; background: var(--ink); color: var(--deep); font-family: var(--font-mono); font-size: 0.85rem; } + +/* numbers */ +.gs-number { display: flex; flex-wrap: wrap; gap: 1.25rem 2rem; align-items: center; padding: 1rem; background: var(--surface); border: 2px solid var(--border-strong); } +.gs-number .gs-draw { width: 9rem; } +.gs-number-controls { display: grid; gap: 0.6rem; min-width: min(100%, 16rem); flex: 1; } +.gs-number-controls input[type=range] { width: 100%; accent-color: var(--them); } +.gs-number-word { font-family: var(--font-mono); color: var(--them); } +.gs-bits { display: grid; grid-template-columns: repeat(3, 3rem); gap: 2px; font-family: var(--font-mono); font-size: 0.75rem; text-align: center; } +.gs-bits span { padding: 0.25rem 0; background: var(--deep); color: var(--muted); } +.gs-bits span.on { background: var(--them); color: var(--them-ink); } + +/* the dictionary */ +.gs-search { display: grid; gap: 0.9rem; margin-bottom: 1.5rem; } +.gs-search .field { max-width: 32rem; } +.gs-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; } +.gs-chips button { font: inherit; font-size: 0.85rem; padding: 0.25rem 0.7rem; color: var(--muted); background: none; border: 2px solid var(--border-strong); cursor: pointer; } +.gs-chips button:hover { color: var(--text); border-color: var(--text); } +.gs-chips button[aria-pressed="true"] { color: var(--them-ink); background: var(--them); border-color: var(--them); } +.gs-count { font-family: var(--font-mono); font-size: 0.8rem; color: var(--muted); } +.gs-built { display: flex; gap: 1.25rem; align-items: center; margin-bottom: 1.25rem; padding: 1rem; background: var(--surface-2); border: 2px solid var(--us); box-shadow: 6px 6px 0 var(--us-soft); } +.gs-built[hidden] { display: none; } +.gs-built .kicker { margin: 0 0 0.25rem; color: var(--us); } +.gs-built p { margin: 0; } +.gs-entries { display: grid; gap: 0.6rem; grid-template-columns: repeat(auto-fill, minmax(min(100%, 21rem), 1fr)); list-style: none; margin: 0; padding: 0; } +.gs-entry { display: flex; gap: 1rem; align-items: flex-start; padding: 0.75rem; background: var(--surface); border: 2px solid var(--border); scroll-margin-top: 5rem; } +.gs-entry:target { border-color: var(--them); } +.gs-entry-text { display: grid; gap: 0.2rem; min-width: 0; } +.gs-entry-text p, .gs-built .gs-entry-head { margin: 0; } +.gs-entry-head { display: flex; flex-wrap: wrap; gap: 0.25rem 0.6rem; align-items: baseline; } +.gs-entry-head strong { font-size: 1.05rem; } +.gs-entry-en, .gs-entry-note { font-size: 0.88rem; color: var(--muted); } +.gs-entry-meta { display: flex; flex-wrap: wrap; gap: 0.2rem 0.9rem; font-size: 0.78rem; color: var(--muted); } +.gs-entry-meta span { font-family: var(--font-mono); } +.gs-none { color: var(--muted); } +.gs-parts-table .gs-draw-part { width: 2.25rem; } +.gs-parts-table td { vertical-align: middle; } + +/* the converter */ +.gs-pane { display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); gap: 1.5rem; align-items: start; } +.gs-pane > div { display: grid; gap: 0.9rem; min-width: 0; align-content: start; } +.gs-pane .field textarea { font-family: var(--font-mono); font-size: 0.88rem; min-height: 10rem; white-space: pre; overflow-x: auto; } +#gs-drawing { min-height: 12rem; line-height: 1.15; letter-spacing: 0.08em; } +.gs-error { margin: 0; padding: 0.5rem 0.75rem; border: 2px solid var(--us); color: var(--us); background: var(--us-soft); font-size: 0.9rem; } +.gs-out .gs-draw { max-width: 28rem; } +.gs-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; } +.gs-editor { touch-action: none; cursor: crosshair; max-width: 30rem; } +.gs-palette { display: grid; grid-template-columns: repeat(auto-fill, minmax(4.4rem, 1fr)); gap: 0.35rem; } +.gs-palette button { display: grid; justify-items: center; gap: 0.25rem; padding: 0.35rem 0.2rem; font: inherit; font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.04em; color: var(--muted); background: var(--deep); border: 2px solid var(--border); cursor: pointer; } +.gs-palette button .gs-draw-part { width: 1.6rem; border: 0; } +.gs-palette button:hover { color: var(--text); border-color: var(--text); } +.gs-palette button[aria-pressed="true"] { color: var(--text); border-color: var(--them); background: var(--surface-2); } +.gs-controls { display: flex; flex-wrap: wrap; gap: 0.75rem 1.25rem; align-items: end; } +.gs-controls .field { min-width: 6rem; } +.gs-symbols { display: flex; flex-wrap: wrap; gap: 0.25rem 0.9rem; align-items: center; margin: 0; padding: 0; border: 0; } +.gs-symbols legend { padding: 0; margin-bottom: 0.35rem; font-size: 0.9rem; color: var(--muted); } +.gs-symbols label { display: inline-flex; gap: 0.35rem; align-items: center; font-size: 0.92rem; } +.gs-symbols input[type=text] { width: 2.4rem; padding: 0.15rem 0.3rem; font: inherit; font-family: var(--font-mono); text-align: center; color: var(--text); background: var(--deep); border: 2px solid var(--border-strong); } +.gs-ink-them { color: var(--them); font-family: var(--font-mono); } +.gs-ink-us { color: var(--us); font-family: var(--font-mono); } +.gs-ink-third { color: var(--c4); font-family: var(--font-mono); } +.gs-steps { display: grid; gap: 0.5rem; margin: 0; padding-left: 1.4rem; max-width: 44rem; } + +@media (max-width: 52rem) { + .gs-example, .gs-pane { grid-template-columns: minmax(0, 1fr); } +} diff --git a/app/script.js b/app/script.js new file mode 100644 index 0000000..8021a1b --- /dev/null +++ b/app/script.js @@ -0,0 +1,396 @@ +// The grid script pages: the explainer, the dictionary and the converter (one script for all three). +// Everything about the script itself is in lib/glyph.js (a port of glyph-cli); this file only draws it +// and wires up the controls. The page says which parts it has with data-gs-* attributes. +import { + Vocabulary, GlyphError, parsePage, formatPage, render, renderSvg, rowsSvg, decode, readGraph, bandRows, + pageSymbols, wordStr, isEmpty, structure, ink, numberShape, WIDTH, POSITIONS, EMPTY_CELL, BACKGROUND, GRID, +} from '../lib/glyph.js'; + +const zh = document.documentElement.lang.toLowerCase().startsWith('zh'); +const $ = (s, el = document) => el.querySelector(s); +const $$ = (s, el = document) => [...el.querySelectorAll(s)]; +const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); +const LIB = new URL('../lib/', import.meta.url); +const HERE = new URL('./', location.href); +// the section's pages: CN at script/…/, EN at script/…/en/ +const SECTION = new URL('../script/', import.meta.url); +const pageUrl = (name, source) => { + const url = new URL(`${name ? `${name}/` : ''}${zh ? '' : 'en/'}`, SECTION); + if (source !== undefined) url.searchParams.set('s', source); + return url.href; +}; + +const T = zh ? { + slots: { subject: '前节点', relation: '关系', object: '后节点' }, halves: { kind: '类', which: '指' }, + meaning: { yes: '是', no: '不', instead: '换成', 'and also': '还有' }, + to: (s) => `(回应 ${s})`, quote: (s) => `“${s}”`, noComment: '在场,未表态', + statement: (n) => `[第 ${n} 句]`, thatNothing: '[“那”:上面没有句子]', notInVocabulary: '(不在词典中)', + part: '部件', asThing: '作为事物', asRelation: '作为关系', none: '—', + results: (n) => `${n} 个词`, noResults: '没有找到。试试别的词,或者直接写出一个词,比如 BODY.STAR。', + composed: '拼出来的词', inVocab: '词典里的词', notIn: '还不在词典里:这是按部件拼出的读法。', number: '数', numberNote: '数是内置的:COUNT 后面的九个比特。', + all: '全部', draw: '在转换器里画', copied: '已复制', copy: '复制', linkCopied: '链接已复制', + empty: '空', voice: (s) => `声部 ${s}`, edges: '读出的图', nodes: '节点', + errLine: (n) => `第 ${n} 行:`, + err: { + unknownPart: (a) => `未知的部件:${a.part}`, range: (a) => `数超出 0–511:${a.n}`, + numberKind: (a) => `数要以 COUNT 为类,比如 COUNT.${a.n}(写的是 ${a.s})`, + countWhich: (a) => `COUNT 的“指”只能是数,比如 COUNT.12(写的是 ${a.s})`, + symbol: (a) => `符号是一个字符,不能是空格或 # . : _ |(写的是 “${a.symbol}”)`, + format: () => '应写成“符号: 节点 | 关系 | 节点”', slots: (a) => `要正好三个位置,用 | 分开,现在是 ${a.n} 个`, + width: (a) => `第 ${a.row} 行有 ${a.len} 格宽;文字总是 23 格(不在格点上:是一幅图吗?)`, + between: (a) => `第 ${a.row} 行第 ${a.col} 列:标记落在两个位置之间(是一幅图吗?)`, + structure: () => '这些行排不成句子和声部;这是一幅图,而不是文字吗?', + mixed: (a) => `第 ${a.from}–${a.to} 行:一个声部里混用了几种符号`, + notPart: (a) => `第 ${a.from}–${a.to} 行,第 ${a.pos} 个位置:不是部件`, + notWhich: (a) => `第 ${a.from}–${a.to} 行,第 ${a.pos} 个位置:这里既不是部件,也不是数`, + }, +} : { + slots: { subject: 'subject', relation: 'relation', object: 'object' }, halves: { kind: 'kind', which: 'which' }, + meaning: { yes: 'yes', no: 'no', instead: 'instead', 'and also': 'and also' }, + to: (s) => ` (to ${s})`, quote: (s) => `"${s}"`, noComment: 'present, no comment', + statement: (n) => `[statement ${n}]`, thatNothing: '[that: nothing above]', notInVocabulary: '(not in vocabulary)', + part: 'part', asThing: 'as a thing', asRelation: 'as a relation', none: '—', + results: (n) => `${n} word${n === 1 ? '' : 's'}`, noResults: 'Nothing found. Try another word, or write one out, like BODY.STAR.', + composed: 'A word you built', inVocab: 'In the dictionary', notIn: 'Not in the dictionary yet: this is how its parts read.', number: 'A number', numberNote: 'Numbers are built in: nine bits after COUNT.', + all: 'All', draw: 'Draw in the converter', copied: 'Copied', copy: 'Copy', linkCopied: 'Link copied', + empty: 'empty', voice: (s) => `voice ${s}`, edges: 'The graph it reads', nodes: 'Nodes', + errLine: () => '', err: {}, +}; + +const message = (err) => { + if (!(err instanceof GlyphError) || !zh || !T.err[err.code]) return err.message; + return (err.args.line ? T.errLine(err.args.line) : '') + T.err[err.code](err.args); +}; + +// ------------------------------------------------------------------ data + +async function loadVocab() { + const get = (f) => fetch(new URL(f, LIB)).then((r) => { if (!r.ok) throw new Error(`${f}: ${r.status}`); return r.json(); }); + const [data, z] = await Promise.all([get('glyph-vocab.json'), get('glyph-zh.json')]); + const en = new Vocabulary(data); + if (!zh) return { en, voc: en, z: null }; + const local = structuredClone(data); + for (const [name, p] of Object.entries(local.parts)) Object.assign(p, z.parts[name] || {}); + local.markers = { ...local.markers, ...z.markers }; + for (const w of local.words) { + const t = z.words[wordStr({ kind: w.kind, which: w.which })] || {}; + w.gloss = t.gloss ?? w.gloss; w.note = t.note ?? w.note; w.domain = z.domains[w.domain] ?? w.domain; + } + const voc = new Vocabulary(local, { + notInVocabulary: T.notInVocabulary, statement: T.statement, thatNothing: T.thatNothing, quote: T.quote, to: T.to, + }); + return { en, voc, z }; +} + +// ------------------------------------------------------------------ drawing helpers + +const svgOf = (rows, symbols, cell = 12, width = WIDTH) => rowsSvg(rows, symbols, { cell, width }); +const wordSvg = (vocab, w, symbol = '+') => svgOf(bandRows(vocab, [w.kind, w.which], symbol), [symbol], 12, 7); +const partSvg = (shape, symbol = '+') => svgOf(shape.map((r) => r.replaceAll('#', symbol)), [symbol], 12, 3); + +function graphHtml(voc, g, symbols) { + const label = (n) => (n.statement !== null ? (n.statement ? T.statement(n.statement) : T.thatNothing) : n.gloss); + const sym = (s) => `${esc(s)}`; + return `
    ${g.edges.map((e) => `
  1. +

    ${sym(e.symbol)} ${esc(label(e.subject))} + ${esc(e.relation || T.none)} + ${esc(label(e.object))}

    + ${e.replies.map((r) => `

    ${sym(r.symbol)} ${r.responses.length ? r.responses.map((x) => + `${T.slots[x.slot]}.${T.halves[x.half]} ${T.meaning[x.meaning]}${x.meaning === 'yes' || x.meaning === 'no' ? '' : ' ' + esc(T.quote(x.part))}${x.to ? esc(T.to(x.to)) : ''}`).join('') : `${T.noComment}`}

    `).join('')} +
  2. `).join('')}
`; +} + +async function copyText(text, btn, done = T.copied) { + try { await navigator.clipboard.writeText(text); } catch { return; } + const old = btn.textContent; btn.textContent = done; setTimeout(() => { btn.textContent = old; }, 1400); +} + +// ------------------------------------------------------------------ the explainer + +function explainer({ en, voc }) { + for (const el of $$('[data-gs-parts]')) { + el.innerHTML = Object.values(voc.parts).map((p) => `
  • +
    ${partSvg(p.shape)}
    +
    ${p.name}${esc(p.thing)}${p.relation ? `${esc(p.relation)}` : ''}
  • `).join(''); + } + for (const el of $$('[data-gs-words]')) { + el.innerHTML = el.dataset.gsWords.split(/\s+/).filter(Boolean).map((s) => { + const w = en.parse(s); + return `
  • ${wordSvg(en, w)}
    ${wordStr(w)}${esc(voc.gloss(w))}
  • `; + }).join(''); + } + for (const el of $$('[data-gs-example]')) { + const source = el.dataset.gsExample.replaceAll('\\n', '\n'); + const page = parsePage(source, en); + el.innerHTML = `
    ${renderSvg(en, page, { cell: 14 })}
    +
    ${esc(formatPage(page).trim())}
    ${graphHtml(voc, readGraph(voc, page), pageSymbols(page))} + ${zh ? '在转换器里打开 →' : 'Open in the converter →'}
    `; + } + for (const el of $$('[data-gs-number]')) { + const input = $('input[type=range]', el), num = $('input[type=number]', el), out = $('.gs-draw', el), bits = $('.gs-bits', el); + const show = (n) => { + n = Math.max(0, Math.min(511, Math.round(+n || 0))); + input.value = num.value = n; + const w = en.parse(`COUNT.${n}`); + out.innerHTML = wordSvg(en, w); + const sh = numberShape(n); + const vals = [256, 128, 64, 32, 16, 8, 4, 2, 1]; + bits.innerHTML = sh.join('').split('').map((c, i) => `${vals[i]}`).join(''); + $('.gs-number-word', el).textContent = wordStr(w); + }; + input.addEventListener('input', () => show(input.value)); + num.addEventListener('input', () => show(num.value)); + show(137); + } +} + +// ------------------------------------------------------------------ the dictionary + +function dictionary({ en, voc }) { + const q = $('#gs-q'), list = $('#gs-results'), count = $('#gs-count'), chips = $('#gs-domains'), built = $('#gs-built'); + const parts = $('#gs-parts-table'); + if (parts) { + parts.innerHTML = Object.values(voc.parts).map((p) => `
    ${partSvg(p.shape)}
    + ${p.name}${esc(p.thing)}${esc(p.relation || T.none)}`).join(''); + } + const entries = voc.entries.map((e, i) => ({ e, en: en.entries[i] })); + const domains = voc.domains(); + let domain = ''; + chips.innerHTML = [``, + ...domains.map((d) => ``)].join(''); + chips.addEventListener('click', (ev) => { + const b = ev.target.closest('button'); if (!b) return; + domain = b.dataset.d; + $$('button', chips).forEach((x) => x.setAttribute('aria-pressed', String(x === b))); + update(); + }); + const card = ({ e, en: ee }) => { + const code = wordStr(e.word); + return `
  • +
    ${wordSvg(en, e.word)}
    +
    +

    ${esc(e.gloss)}${code}

    + ${zh ? `

    ${esc(ee.gloss)}

    ` : ''} + ${e.note ? `

    ${esc(e.note)}

    ` : ''} + +
  • `; + }; + const update = () => { + const t = q.value.trim().toLowerCase(); + // a word written out (BODY.STAR, count.42): show how it draws and reads, in the dictionary or not + built.hidden = true; + if (t && /^[a-z_]+(\.[a-z0-9_]*)?$/i.test(t)) { + try { + const w = en.parse(t); + if (!isEmpty(w)) { + const known = en.lookup(w); + const num = w.kind === 'COUNT'; + built.hidden = false; + built.innerHTML = `
    ${wordSvg(en, w)}
    +

    ${num ? T.number : known ? T.inVocab : T.composed}

    +

    ${esc(voc.gloss(w))}${wordStr(w)}

    + ${known ? '' : `

    ${num ? T.numberNote : T.notIn}

    `} +
    `; + } + } catch { /* not a word: search the meanings instead */ } + } + const hits = entries.filter(({ e, en: ee }) => (!domain || e.domain === domain) && (!t || [ + wordStr(e.word), e.gloss, e.note, ee.gloss, ee.note, + ].some((s) => s.toLowerCase().includes(t)))); + list.innerHTML = hits.map(card).join('') || `
  • ${T.noResults}
  • `; + count.textContent = T.results(hits.length); + }; + q.addEventListener('input', update); + const initial = new URLSearchParams(location.search).get('q'); + if (initial) q.value = initial; + update(); + if (location.hash) document.getElementById(decodeURIComponent(location.hash.slice(1)))?.scrollIntoView(); +} + +// ------------------------------------------------------------------ the converter + +const SYMBOLS = ['+', '×', '*']; + +function converter({ en, voc }) { + const src = $('#gs-src'), out = $('#gs-out'), rowsOut = $('#gs-rows'), graphOut = $('#gs-graph'), errOut = $('#gs-src-err'); + const examples = $('#gs-examples'); + let lastRows = []; + + // ---- source -> drawing + const fromSource = () => { + try { + const page = parsePage(src.value, en); + lastRows = render(en, page); + out.innerHTML = page.lines.length ? renderSvg(en, page, { cell: 16 }) : ''; + rowsOut.textContent = lastRows.join('\n'); + graphOut.innerHTML = page.lines.length ? graphHtml(voc, readGraph(voc, page), pageSymbols(page)) : ''; + errOut.hidden = true; + src.removeAttribute('aria-invalid'); + } catch (err) { + errOut.textContent = message(err); errOut.hidden = false; + src.setAttribute('aria-invalid', 'true'); + } + }; + src.addEventListener('input', fromSource); + examples.addEventListener('change', () => { + const opt = examples.selectedOptions[0]; + if (!opt.value) return; + src.value = opt.value.replaceAll('\\n', '\n'); fromSource(); + }); + $('#gs-copy-rows').addEventListener('click', (ev) => copyText(lastRows.join('\n') + '\n', ev.currentTarget)); + $('#gs-svg').addEventListener('click', () => { + try { + const blob = new Blob([renderSvg(en, parsePage(src.value, en))], { type: 'image/svg+xml' }); + const a = Object.assign(document.createElement('a'), { href: URL.createObjectURL(blob), download: 'grid-script.svg' }); + a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); + } catch { /* the error is already on screen */ } + }); + $('#gs-link').addEventListener('click', (ev) => { + const url = new URL(HERE); url.search = ''; url.searchParams.set('s', src.value); + history.replaceState(null, '', url); + copyText(url.href, ev.currentTarget, T.linkCopied); + }); + $('#gs-to-editor').addEventListener('click', () => { + if (!lastRows.length) return; + drawing.value = lastRows.join('\n'); fromDrawing(true); + $('#reverse').scrollIntoView({ behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' }); + }); + + // ---- drawing -> source: an editor grid, mirrored in a text area + const drawing = $('#gs-drawing'), editor = $('#gs-editor'), back = $('#gs-back'), backGraph = $('#gs-back-graph'), backErr = $('#gs-back-err'); + const linesIn = $('#gs-lines'), voicesIn = $('#gs-voices'), palette = $('#gs-palette'), numIn = $('#gs-num'); + let grid = []; // rows of characters + let symbol = '+'; + let tool = 'BODY'; // a part name, 'NUMBER', 'CELL' or 'ERASE' + const CELL = 18; + + const heightFor = (lines, voices) => lines * (4 * voices - 1) + LINE_GAP_ROWS * (lines - 1); + const LINE_GAP_ROWS = 3; + const resize = (h) => { + const blank = () => Array(WIDTH).fill(EMPTY_CELL); + grid = Array.from({ length: h }, (_, r) => grid[r] || blank()); + }; + const drawEditor = () => { + const symbols = [...new Set(SYMBOLS.concat(grid.flat().filter((c) => c !== EMPTY_CELL)))]; + // lattice positions are a shade lighter, so the grid shows where parts go + const voices = +voicesIn.value, period = 4 * voices - 1 + LINE_GAP_ROWS; + const w = (WIDTH + 2) * CELL, h = (grid.length + 2) * CELL, size = CELL - 2; + const out = [``]; + grid.forEach((row, r) => { + const at = r % period; + const onRow = at < 4 * voices - 1 && at % 4 !== 3; + row.forEach((ch, c) => { + const fill = ch !== EMPTY_CELL ? ink(ch, symbols) : onRow && c % 4 !== 3 ? '#26339a' : GRID; + out.push(``); + }); + }); + editor.innerHTML = out.join('') + ''; + drawing.value = grid.map((r) => r.join('')).join('\n'); + }; + const fromDrawing = (load = false) => { + if (load) { + const rows = drawing.value.split(/\r?\n/).map((r) => r.trim()).filter(Boolean); + if (rows.length && rows.every((r) => [...r].length === WIDTH)) { + grid = rows.map((r) => [...r]); + // the voices per line as the machine reads them, so the editor shades the right positions + try { + const v = structure(rows); + if (v <= 3) { voicesIn.value = v; linesIn.value = (rows.length + LINE_GAP_ROWS) / (4 * v - 1 + LINE_GAP_ROWS); } + } catch { /* not text (yet): keep the layout */ } + drawEditor(); + } + } + try { + const page = decode(en, drawing.value); + back.textContent = formatPage(page); + backGraph.innerHTML = page.lines.length ? graphHtml(voc, readGraph(voc, page), pageSymbols(page)) : ''; + backErr.hidden = true; + } catch (err) { + back.textContent = ''; backGraph.innerHTML = ''; + backErr.textContent = message(err); backErr.hidden = false; + } + }; + const relayout = () => { resize(heightFor(+linesIn.value, +voicesIn.value)); drawEditor(); fromDrawing(); }; + linesIn.addEventListener('change', () => { linesIn.value = Math.max(1, Math.min(12, Math.round(+linesIn.value || 1))); relayout(); }); + voicesIn.addEventListener('change', relayout); + drawing.addEventListener('input', () => fromDrawing(true)); + $('#gs-clear').addEventListener('click', () => { grid = []; relayout(); }); + + // the palette: one button per part, a number, a single cell, the eraser + const tools = [...Object.values(en.parts).map((p) => ({ id: p.name, label: p.name, shape: p.shape })), + { id: 'NUMBER', label: zh ? '数' : 'number', shape: numberShape(5) }, + { id: 'CELL', label: zh ? '单格' : 'one cell', shape: ['...', '.#.', '...'] }, + { id: 'ERASE', label: zh ? '擦除' : 'erase', shape: ['...', '...', '...'] }]; + palette.innerHTML = tools.map((t) => ``).join(''); + palette.addEventListener('click', (ev) => { + const b = ev.target.closest('button'); if (!b) return; + tool = b.dataset.tool; + $$('button', palette).forEach((x) => x.setAttribute('aria-pressed', String(x === b))); + numIn.closest('.field').hidden = tool !== 'NUMBER'; + }); + numIn.closest('.field').hidden = true; + for (const r of $$('input[name=gs-symbol]')) r.addEventListener('change', () => { symbol = r.value === 'other' ? ($('#gs-own').value || '*') : r.value; }); + $('#gs-own').addEventListener('input', (ev) => { + const v = [...ev.target.value].pop(); + if (!v || '.#|:_ '.includes(v)) return; + ev.target.value = v; + $('input[name=gs-symbol][value=other]').checked = true; symbol = v; + }); + + const paint = (ev, dragging) => { + const box = editor.firstElementChild.getBoundingClientRect(); + const scale = box.width / ((WIDTH + 2) * CELL); + const c = Math.floor((ev.clientX - box.left) / scale / CELL) - 1; + const r = Math.floor((ev.clientY - box.top) / scale / CELL) - 1; + if (r < 0 || c < 0 || r >= grid.length || c >= WIDTH) return; + if (tool === 'CELL') { + if (dragging) grid[r][c] = paint.mode; + else { paint.mode = grid[r][c] === EMPTY_CELL ? symbol : EMPTY_CELL; grid[r][c] = paint.mode; } + } else { + if (dragging) return; + // stamp a whole part into the lattice position under the pointer + const p = Math.floor(c / 4); + const voices = +voicesIn.value, period = 4 * voices - 1 + LINE_GAP_ROWS; + const at = r % period; + if (at >= 4 * voices - 1) return; + const top = r - (at % 4); + const shape = tool === 'ERASE' ? ['...', '...', '...'] : tool === 'NUMBER' + ? numberShape(Math.max(0, Math.min(511, Math.round(+numIn.value || 0)))) : en.parts[tool].shape; + const ch = tool === 'ERASE' ? EMPTY_CELL : symbol; + for (let i = 0; i < 3; i++) for (let j = 0; j < 3; j++) { + if (top + i < grid.length) grid[top + i][p * 4 + j] = shape[i][j] === '#' ? ch : EMPTY_CELL; + } + } + drawEditor(); fromDrawing(); + }; + editor.addEventListener('pointerdown', (ev) => { ev.preventDefault(); editor.setPointerCapture(ev.pointerId); paint(ev, false); }); + editor.addEventListener('pointermove', (ev) => { if (ev.buttons && tool === 'CELL') paint(ev, true); }); + $('#gs-to-source').addEventListener('click', () => { + if (!back.textContent) return; + src.value = back.textContent; fromSource(); + $('#forward').scrollIntoView({ behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' }); + }); + + // start: a shared link, or the first example; the editor starts from its drawing + const shared = new URLSearchParams(location.search).get('s'); + src.value = shared ?? examples.options[1].value.replaceAll('\\n', '\n'); + fromSource(); + drawing.value = lastRows.join('\n'); + fromDrawing(true); + if (!grid.length) relayout(); +} + +// ------------------------------------------------------------------ start + +const main = $('main[data-gs]'); +loadVocab().then((v) => { + const page = main?.dataset.gs; + if (page === 'explainer') explainer(v); + if (page === 'dictionary') dictionary(v); + if (page === 'converter') converter(v); +}).catch((err) => { + console.error(err); + const box = $('[data-gs-fail]'); + if (box) box.hidden = false; +}); diff --git a/kit/nav.cn.json b/kit/nav.cn.json index c75b427..8ae3c89 100644 --- a/kit/nav.cn.json +++ b/kit/nav.cn.json @@ -30,6 +30,12 @@ "url": "https://interstellar.interimm.org/signals/", "children": [] }, + { + "id": "script", + "name": "网格文字", + "url": "https://interstellar.interimm.org/script/", + "children": [] + }, { "id": "pulsars", "name": "脉冲星账本", @@ -86,6 +92,10 @@ "title": "五段发射(可收听)", "url": "https://interstellar.interimm.org/signals/" }, + { + "title": "网格文字 · 词典与转换器", + "url": "https://interstellar.interimm.org/script/" + }, { "title": "脉冲星账本(实时)", "url": "https://interstellar.interimm.org/pulsars/" diff --git a/kit/nav.en.json b/kit/nav.en.json index 9e7c74b..2d3ec03 100644 --- a/kit/nav.en.json +++ b/kit/nav.en.json @@ -30,6 +30,12 @@ "url": "https://interstellar.interimm.org/signals/en/", "children": [] }, + { + "id": "script", + "name": "Grid script", + "url": "https://interstellar.interimm.org/script/en/", + "children": [] + }, { "id": "pulsars", "name": "Pulsar ledger", @@ -86,6 +92,10 @@ "title": "Five transmissions (listen)", "url": "https://interstellar.interimm.org/signals/en/" }, + { + "title": "Grid script · dictionary and converter", + "url": "https://interstellar.interimm.org/script/en/" + }, { "title": "Pulsar ledger (live)", "url": "https://interstellar.interimm.org/pulsars/en/" diff --git a/lib/glyph-vocab.json b/lib/glyph-vocab.json new file mode 100644 index 0000000..fa3d907 --- /dev/null +++ b/lib/glyph-vocab.json @@ -0,0 +1,545 @@ +{ + "version": 1, + "parts": { + "SELF": { + "shape": [ + "###", + "#.#", + "###" + ], + "thing": "we", + "relation": "know" + }, + "OTHER": { + "shape": [ + "###", + "#..", + "###" + ], + "thing": "you", + "relation": "answer" + }, + "STAR": { + "shape": [ + ".#.", + "###", + ".#." + ], + "thing": "star", + "relation": "shine on" + }, + "LIGHT": { + "shape": [ + "#.#", + ".#.", + "#.#" + ], + "thing": "light", + "relation": "see" + }, + "BODY": { + "shape": [ + "###", + "###", + "###" + ], + "thing": "world, matter", + "relation": "have, contain" + }, + "AIR": { + "shape": [ + "#.#", + "...", + "#.#" + ], + "thing": "air", + "relation": "carry" + }, + "WATER": { + "shape": [ + "###", + "...", + "###" + ], + "thing": "water", + "relation": "flow to" + }, + "TIME": { + "shape": [ + "#.#", + "#.#", + "#.#" + ], + "thing": "time", + "relation": "last, wait" + }, + "VOICE": { + "shape": [ + "#..", + "##.", + "###" + ], + "thing": "sound", + "relation": "say, send" + }, + "PATH": { + "shape": [ + "...", + "###", + "..." + ], + "thing": "way, between", + "relation": "go to, reach" + }, + "NOT": { + "shape": [ + "#..", + ".#.", + "..#" + ], + "thing": "not", + "relation": null + }, + "ONE": { + "shape": [ + "...", + ".#.", + "..." + ], + "thing": "one, this, the same", + "relation": "is, equals" + }, + "BEFORE": { + "shape": [ + "...", + "#..", + "..." + ], + "thing": "earlier", + "relation": null + }, + "AFTER": { + "shape": [ + "...", + "..#", + "..." + ], + "thing": "later", + "relation": null + }, + "OPEN": { + "shape": [ + ".#.", + "#.#", + ".#." + ], + "thing": "what goes here?", + "relation": null + }, + "COUNT": { + "shape": [ + "...", + "...", + "###" + ], + "thing": "number", + "relation": null + } + }, + "markers": { + "_": "no tense", + "BEFORE": "past", + "ONE": "now", + "AFTER": "future", + "PATH": "always", + "NOT": "not", + "OPEN": "asked: does it?" + }, + "words": [ + { + "kind": "SELF", + "which": null, + "gloss": "we", + "domain": "People and voices", + "note": "the one drawing" + }, + { + "kind": "OTHER", + "which": null, + "gloss": "you", + "domain": "People and voices", + "note": "the one reading" + }, + { + "kind": "OTHER", + "which": "OTHER", + "gloss": "a third party", + "domain": "People and voices", + "note": "\"another you\"" + }, + { + "kind": "ONE", + "which": "SELF", + "gloss": "one of us", + "domain": "People and voices", + "note": "an individual" + }, + { + "kind": "ONE", + "which": "OTHER", + "gloss": "one of you", + "domain": "People and voices", + "note": "a human, to them" + }, + { + "kind": "VOICE", + "which": "SELF", + "gloss": "our voice", + "domain": "People and voices", + "note": "" + }, + { + "kind": "VOICE", + "which": "OTHER", + "gloss": "your voice", + "domain": "People and voices", + "note": "" + }, + { + "kind": "STAR", + "which": null, + "gloss": "star", + "domain": "Sky", + "note": "" + }, + { + "kind": "STAR", + "which": "SELF", + "gloss": "our star", + "domain": "Sky", + "note": "Ross 128" + }, + { + "kind": "STAR", + "which": "OTHER", + "gloss": "your star", + "domain": "Sky", + "note": "the Sun" + }, + { + "kind": "STAR", + "which": "TIME", + "gloss": "pulsar", + "domain": "Sky", + "note": "a timing star" + }, + { + "kind": "STAR", + "which": "STAR", + "gloss": "galaxy", + "domain": "Sky", + "note": "star of stars" + }, + { + "kind": "AIR", + "which": "NOT", + "gloss": "space", + "domain": "Sky", + "note": "no air" + }, + { + "kind": "LIGHT", + "which": "PATH", + "gloss": "ray, signal", + "domain": "Sky", + "note": "light going" + }, + { + "kind": "LIGHT", + "which": "BODY", + "gloss": "transit, shadow", + "domain": "Sky", + "note": "light with a world in it" + }, + { + "kind": "BODY", + "which": null, + "gloss": "world, matter", + "domain": "Worlds", + "note": "" + }, + { + "kind": "BODY", + "which": "SELF", + "gloss": "our world", + "domain": "Worlds", + "note": "Ross 128 b" + }, + { + "kind": "BODY", + "which": "OTHER", + "gloss": "your world", + "domain": "Worlds", + "note": "Earth" + }, + { + "kind": "BODY", + "which": "AIR", + "gloss": "air-world", + "domain": "Worlds", + "note": "" + }, + { + "kind": "BODY", + "which": "WATER", + "gloss": "water-world", + "domain": "Worlds", + "note": "" + }, + { + "kind": "BODY", + "which": "BODY", + "gloss": "rock, ground", + "domain": "Worlds", + "note": "matter of matter" + }, + { + "kind": "BODY", + "which": "PATH", + "gloss": "orbit", + "domain": "Worlds", + "note": "a world going" + }, + { + "kind": "LIGHT", + "which": null, + "gloss": "light", + "domain": "Their surface", + "note": "" + }, + { + "kind": "LIGHT", + "which": "NOT", + "gloss": "dark, the night side", + "domain": "Their surface", + "note": "" + }, + { + "kind": "PATH", + "which": "LIGHT", + "gloss": "terminator", + "domain": "Their surface", + "note": "the way between light and dark" + }, + { + "kind": "AIR", + "which": null, + "gloss": "air", + "domain": "Their surface", + "note": "" + }, + { + "kind": "AIR", + "which": "PATH", + "gloss": "wind", + "domain": "Their surface", + "note": "air going" + }, + { + "kind": "AIR", + "which": "WATER", + "gloss": "cloud", + "domain": "Their surface", + "note": "watery air" + }, + { + "kind": "WATER", + "which": null, + "gloss": "water", + "domain": "Their surface", + "note": "" + }, + { + "kind": "WATER", + "which": "PATH", + "gloss": "river", + "domain": "Their surface", + "note": "water going" + }, + { + "kind": "WATER", + "which": "BODY", + "gloss": "ice", + "domain": "Their surface", + "note": "solid water" + }, + { + "kind": "WATER", + "which": "NOT", + "gloss": "dry land", + "domain": "Their surface", + "note": "" + }, + { + "kind": "TIME", + "which": null, + "gloss": "time", + "domain": "Time", + "note": "" + }, + { + "kind": "TIME", + "which": "BEFORE", + "gloss": "the past, before", + "domain": "Time", + "note": "" + }, + { + "kind": "TIME", + "which": "ONE", + "gloss": "now", + "domain": "Time", + "note": "" + }, + { + "kind": "TIME", + "which": "AFTER", + "gloss": "the future, after", + "domain": "Time", + "note": "" + }, + { + "kind": "TIME", + "which": "PATH", + "gloss": "always", + "domain": "Time", + "note": "time going on" + }, + { + "kind": "TIME", + "which": "NOT", + "gloss": "never", + "domain": "Time", + "note": "" + }, + { + "kind": "TIME", + "which": "BODY", + "gloss": "year", + "domain": "Time", + "note": "one orbit" + }, + { + "kind": "TIME", + "which": "VOICE", + "gloss": "rhythm, beat", + "domain": "Time", + "note": "" + }, + { + "kind": "TIME", + "which": "TIME", + "gloss": "an age", + "domain": "Time", + "note": "time of times" + }, + { + "kind": "VOICE", + "which": null, + "gloss": "sound, voice", + "domain": "Sound and talk", + "note": "" + }, + { + "kind": "VOICE", + "which": "PATH", + "gloss": "message", + "domain": "Sound and talk", + "note": "voice going" + }, + { + "kind": "VOICE", + "which": "NOT", + "gloss": "silence", + "domain": "Sound and talk", + "note": "" + }, + { + "kind": "VOICE", + "which": "VOICE", + "gloss": "conversation", + "domain": "Sound and talk", + "note": "voice with voice" + }, + { + "kind": "VOICE", + "which": "ONE", + "gloss": "monologue", + "domain": "Sound and talk", + "note": "one voice" + }, + { + "kind": "VOICE", + "which": "AIR", + "gloss": "speech", + "domain": "Sound and talk", + "note": "sound in air" + }, + { + "kind": "VOICE", + "which": "LIGHT", + "gloss": "radio", + "domain": "Sound and talk", + "note": "sound carried by light" + }, + { + "kind": "VOICE", + "which": "BODY", + "gloss": "resonator, instrument", + "domain": "Sound and talk", + "note": "a body that sounds" + }, + { + "kind": "VOICE", + "which": "OPEN", + "gloss": "a question", + "domain": "Sound and talk", + "note": "" + }, + { + "kind": "ONE", + "which": null, + "gloss": "that", + "domain": "Logic and number", + "note": "alone in a node slot: the line above" + }, + { + "kind": "ONE", + "which": "ONE", + "gloss": "the same", + "domain": "Logic and number", + "note": "" + }, + { + "kind": "ONE", + "which": "NOT", + "gloss": "different", + "domain": "Logic and number", + "note": "" + }, + { + "kind": "OPEN", + "which": "COUNT", + "gloss": "how many?", + "domain": "Logic and number", + "note": "\"what number?\"; answer with COUNT.n" + }, + { + "kind": "OPEN", + "which": null, + "gloss": "what?", + "domain": "Logic and number", + "note": "" + } + ] +} diff --git a/lib/glyph-zh.json b/lib/glyph-zh.json new file mode 100644 index 0000000..f1b54e7 --- /dev/null +++ b/lib/glyph-zh.json @@ -0,0 +1,90 @@ +{ + "about": "Chinese glosses for lib/glyph-vocab.json, used by the /script/ pages. Keys are the words as glyph-cli writes them.", + "parts": { + "SELF": {"thing": "我们", "relation": "知道"}, + "OTHER": {"thing": "你们", "relation": "回答"}, + "STAR": {"thing": "恒星", "relation": "照耀"}, + "LIGHT": {"thing": "光", "relation": "看见"}, + "BODY": {"thing": "世界、物质", "relation": "拥有、包含"}, + "AIR": {"thing": "空气", "relation": "承载"}, + "WATER": {"thing": "水", "relation": "流向"}, + "TIME": {"thing": "时间", "relation": "持续、等待"}, + "VOICE": {"thing": "声音", "relation": "说、发送"}, + "PATH": {"thing": "路、之间", "relation": "去、到达"}, + "NOT": {"thing": "不", "relation": null}, + "ONE": {"thing": "一、这、同一", "relation": "是、等于"}, + "BEFORE": {"thing": "之前", "relation": null}, + "AFTER": {"thing": "之后", "relation": null}, + "OPEN": {"thing": "这里填什么?", "relation": null}, + "COUNT": {"thing": "数", "relation": null} + }, + "markers": { + "_": "无时态", "BEFORE": "过去", "ONE": "现在", "AFTER": "将来", "PATH": "一直", "NOT": "否定", "OPEN": "问:是否?" + }, + "domains": { + "People and voices": "人与声音", + "Sky": "天空", + "Worlds": "世界", + "Their surface": "他们的地表", + "Time": "时间", + "Sound and talk": "声音与交谈", + "Logic and number": "逻辑与数" + }, + "words": { + "SELF": {"gloss": "我们", "note": "画的一方"}, + "OTHER": {"gloss": "你们", "note": "读的一方"}, + "OTHER.OTHER": {"gloss": "第三方", "note": "“另一个你们”"}, + "ONE.SELF": {"gloss": "我们中的一个", "note": "一个个体"}, + "ONE.OTHER": {"gloss": "你们中的一个", "note": "在他们看来:一个人类"}, + "VOICE.SELF": {"gloss": "我们的声音", "note": ""}, + "VOICE.OTHER": {"gloss": "你们的声音", "note": ""}, + "STAR": {"gloss": "恒星", "note": ""}, + "STAR.SELF": {"gloss": "我们的恒星", "note": "罗斯128"}, + "STAR.OTHER": {"gloss": "你们的恒星", "note": "太阳"}, + "STAR.TIME": {"gloss": "脉冲星", "note": "计时的星"}, + "STAR.STAR": {"gloss": "星系", "note": "星中之星"}, + "AIR.NOT": {"gloss": "太空", "note": "没有空气"}, + "LIGHT.PATH": {"gloss": "光线、信号", "note": "行进的光"}, + "LIGHT.BODY": {"gloss": "凌日、影子", "note": "光里有一个世界"}, + "BODY": {"gloss": "世界、物质", "note": ""}, + "BODY.SELF": {"gloss": "我们的世界", "note": "罗斯128 b"}, + "BODY.OTHER": {"gloss": "你们的世界", "note": "地球"}, + "BODY.AIR": {"gloss": "空气世界", "note": ""}, + "BODY.WATER": {"gloss": "水世界", "note": ""}, + "BODY.BODY": {"gloss": "岩石、地面", "note": "物质中的物质"}, + "BODY.PATH": {"gloss": "轨道", "note": "行进的世界"}, + "LIGHT": {"gloss": "光", "note": ""}, + "LIGHT.NOT": {"gloss": "黑暗、夜面", "note": ""}, + "PATH.LIGHT": {"gloss": "晨昏线", "note": "明与暗之间的路"}, + "AIR": {"gloss": "空气", "note": ""}, + "AIR.PATH": {"gloss": "风", "note": "行进的空气"}, + "AIR.WATER": {"gloss": "云", "note": "含水的空气"}, + "WATER": {"gloss": "水", "note": ""}, + "WATER.PATH": {"gloss": "河", "note": "行进的水"}, + "WATER.BODY": {"gloss": "冰", "note": "固态的水"}, + "WATER.NOT": {"gloss": "干燥的陆地", "note": ""}, + "TIME": {"gloss": "时间", "note": ""}, + "TIME.BEFORE": {"gloss": "过去、之前", "note": ""}, + "TIME.ONE": {"gloss": "现在", "note": ""}, + "TIME.AFTER": {"gloss": "将来、之后", "note": ""}, + "TIME.PATH": {"gloss": "永远", "note": "流逝的时间"}, + "TIME.NOT": {"gloss": "从不", "note": ""}, + "TIME.BODY": {"gloss": "年", "note": "一次公转"}, + "TIME.VOICE": {"gloss": "节奏、节拍", "note": ""}, + "TIME.TIME": {"gloss": "纪元", "note": "时间中的时间"}, + "VOICE": {"gloss": "声音、嗓音", "note": ""}, + "VOICE.PATH": {"gloss": "消息", "note": "行进的声音"}, + "VOICE.NOT": {"gloss": "寂静", "note": ""}, + "VOICE.VOICE": {"gloss": "对话", "note": "声音与声音"}, + "VOICE.ONE": {"gloss": "独白", "note": "一个声音"}, + "VOICE.AIR": {"gloss": "言语", "note": "空气里的声音"}, + "VOICE.LIGHT": {"gloss": "无线电", "note": "由光承载的声音"}, + "VOICE.BODY": {"gloss": "共鸣体、乐器", "note": "会发声的物体"}, + "VOICE.OPEN": {"gloss": "一个问题", "note": ""}, + "ONE": {"gloss": "那", "note": "单独放在节点位置:指上一行"}, + "ONE.ONE": {"gloss": "相同", "note": ""}, + "ONE.NOT": {"gloss": "不同", "note": ""}, + "OPEN.COUNT": {"gloss": "多少?", "note": "“什么数?”,用 COUNT.n 回答"}, + "OPEN": {"gloss": "什么?", "note": ""} + } +} diff --git a/lib/glyph.js b/lib/glyph.js new file mode 100644 index 0000000..50f6440 --- /dev/null +++ b/lib/glyph.js @@ -0,0 +1,473 @@ +// The grid script, in the browser: a port of glyph-cli 0.2.0 (https://github.com/InterImm/glyph-cli). +// Parts, words, pages, drawings and the knowledge-graph reading behave exactly as the `glyph` command does, +// and lib/glyph-vocab.json is a copy of its bundled vocabulary; tests/glyph.test.mjs checks the two agree. +// No DOM here, so the same file runs under node for the tests. + +export const POSITIONS = 6; +export const WIDTH = POSITIONS * 4 - 1; // 23 cells +export const BAND_GAP = 1; +export const LINE_GAP = 3; +export const EMPTY = '_'; +export const EMPTY_CELL = '.'; +export const MARK = '#'; +export const NUMBER = 'COUNT'; // the kind whose which is a number +export const MAX_NUMBER = 511; +export const SLOTS = ['subject', 'relation', 'object']; +export const HALVES = ['kind', 'which']; +export const RESERVED_SYMBOLS = new Set([...'.#|:_ \t']); + +// Colours of the phase 2 ("137") design: signal blue, white-cyan for them, Sol yellow for us. +export const BACKGROUND = '#0d1457'; +export const GRID = '#141d6e'; +export const INKS = { '+': '#9ff8ff', '×': '#ffd84a', x: '#ffd84a' }; +export const OTHER_INKS = ['#c9b3ff', '#7dffa8', '#ffa45c', '#ffffff']; + +// Fixed words of the readings; a translation can replace them (see Vocabulary.labels). +export const LABELS = { + none: '—', + notInVocabulary: '(not in vocabulary)', + statement: (n) => `[statement ${n}]`, + thatNothing: '[that: nothing above]', + yes: 'yes', no: 'no', instead: 'instead', 'and also': 'and also', + to: (s) => ` (to ${s})`, + quote: (s) => ` "${s}"`, + noComment: 'present, no comment', + edges: 'Edges:', nodes: 'Nodes:', +}; + +/** A problem the reader can fix. `code` and `args` let a page say it in another language. */ +export class GlyphError extends Error { + constructor(message, code = 'error', args = {}) { + super(message); + this.name = 'GlyphError'; + this.code = code; + this.args = args; + } +} + +// ---------- words ---------- +// A word is { kind, which }: kind is a part name or null, which is a part name, a number or null. + +export const word = (kind = null, which = null) => ({ kind, which }); +export const isEmpty = (w) => w.kind === null && w.which === null; +export const isNumber = (w) => typeof w.which === 'number'; +export const halves = (w) => [w.kind, w.which]; +export const sameWord = (a, b) => a.kind === b.kind && a.which === b.which; +export const THAT = word('ONE', null); + +export function wordStr(w) { + if (isEmpty(w)) return EMPTY; + return (w.kind ?? EMPTY) + (w.which === null ? '' : `.${w.which}`); +} + +// ---------- the vocabulary ---------- + +export class Vocabulary { + /** `data` is a vocab.json object; `labels` replaces any of LABELS (for a translation). */ + constructor(data, labels = {}) { + if (!data || typeof data !== 'object' || !data.parts || !data.words) { + throw new GlyphError('not a glyph vocabulary file', 'vocabulary'); + } + this.version = data.version ?? 1; + this.parts = {}; + for (const [name, p] of Object.entries(data.parts)) { + this.parts[name] = { name, shape: [...p.shape], thing: p.thing, relation: p.relation ?? null }; + } + this.markers = { ...(data.markers || {}) }; + this.entries = data.words.map((e) => ({ + word: word(e.kind, e.which), gloss: e.gloss, domain: e.domain ?? 'Unsorted', note: e.note ?? '', + })); + this.labels = { ...LABELS, ...labels }; + } + + /** 'BODY.OTHER' -> {kind: 'BODY', which: 'OTHER'}; 'COUNT.137' -> {kind: 'COUNT', which: 137}. */ + parse(text) { + const s = String(text).trim(); + if (s === '' || s === EMPTY) return word(); + const dot = s.indexOf('.'); + const k = dot < 0 ? s : s.slice(0, dot); + const w = dot < 0 ? '' : s.slice(dot + 1); + const kind = k === '' || k === EMPTY ? null : k.toUpperCase(); + let which; + if (w === '' || w === EMPTY) { + which = null; + } else if (/^[0-9]+$/.test(w)) { + which = parseInt(w, 10); + if (which > MAX_NUMBER) throw new GlyphError(`number out of range 0-${MAX_NUMBER}: ${which}`, 'range', { n: which }); + if (kind !== NUMBER && kind !== null) { + throw new GlyphError(`a number needs ${NUMBER} as its kind, e.g. ${NUMBER}.${which} (got ${s})`, 'numberKind', { n: which, s }); + } + if (kind === NUMBER && which === 0) which = null; // COUNT alone is zero: COUNT.0 draws exactly like it + } else { + which = w.toUpperCase(); + if (kind === NUMBER) throw new GlyphError(`${NUMBER} only takes a number as its which, e.g. ${NUMBER}.12 (got ${s})`, 'countWhich', { s }); + } + for (const half of [kind, which]) { + if (typeof half === 'string' && !(half in this.parts)) throw new GlyphError(`unknown part: ${half}`, 'unknownPart', { part: half }); + } + return word(kind, which); + } + + lookup(w) { + return this.entries.find((e) => sameWord(e.word, w)) || null; + } + + /** The meaning of one half on its own. */ + thing(half) { + if (half === null) return EMPTY; + if (typeof half === 'number') return String(half); + return this.parts[half].thing; + } + + /** The best human reading of a word as a 'node' or as a 'relation'. */ + gloss(w, role = 'node') { + if (isEmpty(w)) return this.labels.none; + if (role === 'relation') { + let base = ''; + if (w.kind) { + const part = this.parts[w.kind]; + base = part.relation || part.thing; + } + if (w.which === null) return base; + const marker = this.markers[String(w.which)] ?? String(w.which); + return `${base} [${marker}]`; + } + if (w.kind === NUMBER && (isNumber(w) || w.which === null)) return String(w.which || 0); + const entry = this.lookup(w); + if (entry) return entry.gloss; + if (w.which === null) return this.parts[w.kind].thing; + return `${this.thing(w.kind)} | ${this.thing(w.which)} ${this.labels.notInVocabulary}`; + } + + search(text) { + const t = text.toLowerCase(); + const words = this.entries.filter((e) => e.gloss.toLowerCase().includes(t) || e.note.toLowerCase().includes(t)); + const parts = Object.values(this.parts).filter((p) => p.thing.toLowerCase().includes(t) || (p.relation && p.relation.toLowerCase().includes(t))); + return { words, parts }; + } + + /** A list of problems; empty means the vocabulary is sound. */ + validate() { + const errors = []; + const shapes = new Map(); + for (const p of Object.values(this.parts)) { + if (p.shape.length !== 3 || p.shape.some((r) => r.length !== 3 || /[^#.]/.test(r))) errors.push(`part ${p.name}: shape must be 3 rows of 3 "#"/"."`); + const key = p.shape.join('/'); + if (shapes.has(key)) errors.push(`parts ${shapes.get(key)} and ${p.name} have the same shape`); + shapes.set(key, p.name); + if (!p.shape.some((r) => r.includes(MARK))) errors.push(`part ${p.name}: shape is empty`); + } + const seen = new Map(); + const glosses = new Map(); + for (const e of this.entries) { + const ws = wordStr(e.word); + for (const half of halves(e.word)) { + if (typeof half === 'string' && !(half in this.parts)) errors.push(`${ws}: unknown part ${half}`); + } + if (e.word.kind === NUMBER) errors.push(`${ws}: ${NUMBER} words are numbers and are built in`); + if (seen.has(ws)) errors.push(`${ws} defined twice ("${seen.get(ws)}" and "${e.gloss}")`); + seen.set(ws, e.gloss); + const g = e.gloss.toLowerCase(); + if (glosses.has(g)) errors.push(`meaning "${e.gloss}" used by ${glosses.get(g)} and ${ws}`); + glosses.set(g, ws); + } + return errors; + } + + domains() { + const out = []; + for (const e of this.entries) if (!out.includes(e.domain)) out.push(e.domain); + return out; + } +} + +// ---------- pages ---------- +// A page is { lines: [{ bands: [{ symbol, words: [w, w, w] }] }] }. The first band of a line is the statement. + +export const bandHalves = (band) => band.words.flatMap(halves); +export const pageVoices = (page) => page.lines.reduce((n, line) => Math.max(n, line.bands.length), 0); + +export function pageSymbols(page) { + const out = []; + for (const line of page.lines) for (const band of line.bands) if (!out.includes(band.symbol)) out.push(band.symbol); + return out; +} + +export function checkSymbol(symbol) { + if ([...symbol].length !== 1 || RESERVED_SYMBOLS.has(symbol)) { + throw new GlyphError(`a symbol is one character, not a space or any of # . : _ |: '${symbol}'`, 'symbol', { symbol }); + } + return symbol; +} + +/** Read page source ("SYMBOL: node | relation | node", one band per line) into a page. */ +export function parsePage(text, vocab) { + const page = { lines: [] }; + let current = { bands: [] }; + text.split(/\r?\n/).forEach((raw, i) => { + const line = raw.split('#', 1)[0].trimEnd(); + if (!line.trim()) { + if (current.bands.length) { page.lines.push(current); current = { bands: [] }; } + return; + } + const at = (err) => new GlyphError(`line ${i + 1}: ${err.message}`, err.code, { ...err.args, line: i + 1 }); + const colon = line.indexOf(':'); + if (colon < 0) { + throw new GlyphError(`line ${i + 1}: expected "SYMBOL: node | relation | node", got '${raw.trim()}'`, 'format', { line: i + 1 }); + } + const symbol = line.slice(0, colon).trim(); + try { checkSymbol(symbol); } catch (err) { throw at(err); } + const cells = line.slice(colon + 1).split('|'); + if (cells.length !== 3) { + throw new GlyphError(`line ${i + 1}: need exactly three slots separated by |, got ${cells.length}`, 'slots', { line: i + 1, n: cells.length }); + } + let words; + try { words = cells.map((c) => vocab.parse(c)); } catch (err) { throw at(err); } + current.bands.push({ symbol, words }); + }); + if (current.bands.length) page.lines.push(current); + return page; +} + +/** Write a page back as page source. */ +export function formatPage(page) { + const blocks = page.lines.map((line) => line.bands.map((b) => `${b.symbol}: ${b.words.map(wordStr).join(' | ')}`).join('\n')); + return blocks.join('\n\n') + (blocks.length ? '\n' : ''); +} + +// ---------- drawing ---------- + +/** Nine bits, 256 ... 1, left to right, top to bottom. */ +export function numberShape(n) { + const bits = n.toString(2).padStart(9, '0'); + return [0, 1, 2].map((r) => [0, 1, 2].map((c) => (bits[r * 3 + c] === '1' ? MARK : EMPTY_CELL)).join('')); +} + +export function shape(vocab, half) { + if (half === null) return [EMPTY_CELL.repeat(3), EMPTY_CELL.repeat(3), EMPTY_CELL.repeat(3)]; + if (typeof half === 'number') return numberShape(half); + return vocab.parts[half].shape; +} + +/** Three rows of text for a run of positions drawn in one symbol. */ +export function bandRows(vocab, hs, symbol) { + const rows = ['', '', '']; + hs.forEach((half, i) => { + const s = shape(vocab, half); + for (let r = 0; r < 3; r++) rows[r] += (i ? EMPTY_CELL : '') + s[r].replaceAll(MARK, symbol); + }); + return rows; +} + +/** Words side by side, as a quick look-up drawing (not a page). */ +export function drawWords(vocab, words, symbol = '+', spacing = 3) { + const blocks = words.map((w) => bandRows(vocab, halves(w), symbol)); + return [0, 1, 2].map((r) => blocks.map((b) => b[r]).join(' '.repeat(spacing))); +} + +/** Draw a page as rows of text. Every line gets as many bands as the page has voices. */ +export function render(vocab, page) { + const blank = EMPTY_CELL.repeat(WIDTH); + const n = pageVoices(page); + const out = []; + page.lines.forEach((line, li) => { + if (li) for (let i = 0; i < LINE_GAP; i++) out.push(blank); + for (let k = 0; k < n; k++) { + if (k) for (let i = 0; i < BAND_GAP; i++) out.push(blank); + if (k < line.bands.length) out.push(...bandRows(vocab, bandHalves(line.bands[k]), line.bands[k].symbol)); + else out.push(blank, blank, blank); + } + }); + return out; +} + +export function ink(symbol, symbols) { + if (symbol in INKS) return INKS[symbol]; + const others = symbols.filter((s) => !(s in INKS)); + return OTHER_INKS[Math.max(0, others.indexOf(symbol)) % OTHER_INKS.length]; +} + +const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]); + +/** Draw rows of text as SVG: square pixels on signal blue, no curves. */ +export function rowsSvg(rows, symbols, { cell = 12, pad = 1, grid = true, width = WIDTH } = {}) { + const w = width + 2 * pad, h = rows.length + 2 * pad; + const out = [ + ``, + ``, + ]; + const inset = Math.max(1, Math.floor(cell / 12)); + const size = cell - 2 * inset; + rows.forEach((row, r) => { + [...row].forEach((ch, c) => { + const box = `x="${(c + pad) * cell + inset}" y="${(r + pad) * cell + inset}" width="${size}" height="${size}"`; + if (ch !== EMPTY_CELL && ch !== ' ') out.push(`${esc(ch)}`); + else if (grid && ch === EMPTY_CELL) out.push(``); + }); + }); + out.push(''); + return out.join('\n') + '\n'; +} + +/** Draw a page as SVG, as `glyph render --svg` does. */ +export function renderSvg(vocab, page, opts = {}) { + return rowsSvg(render(vocab, page), pageSymbols(page), opts); +} + +// ---------- reading a drawing back ---------- + +/** How many bands each line of a drawing has, from where its empty rows fall (throws if they don't fit). */ +export function structure(rows) { + const empty = rows.map((row) => !row.replaceAll(EMPTY_CELL, '')); + const total = rows.length; + for (let n = 1; n <= total; n++) { + const lineH = 4 * n - 1; + const period = lineH + LINE_GAP; + if ((total + LINE_GAP) % period) continue; + let ok = true; + for (let i = 0; i < total; i++) { + const at = i % period; + const gap = at >= lineH || at % 4 === 3; + if (gap && !empty[i]) { ok = false; break; } + } + if (ok) return n; + } + throw new GlyphError("the rows don't fall into lines and bands; is this a picture rather than text?", 'structure'); +} + +/** + * Read a drawing (as made by render) back into a page. A which is read as nine bits only after COUNT, + * or with no kind under a line whose word there is a COUNT word (a reply like _.12); everywhere else + * it must be a part. + */ +export function decode(vocab, text) { + const rows = text.split(/\r?\n/).map((r) => r.trim()).filter(Boolean); + const page = { lines: [] }; + if (!rows.length) return page; + rows.forEach((row, i) => { + const len = [...row].length; + if (len !== WIDTH) { + throw new GlyphError(`row ${i + 1} is ${len} cells wide; text is always ${WIDTH} (off the lattice: a picture?)`, 'width', { row: i + 1, len }); + } + const cells = [...row]; + for (let c = 3; c < WIDTH; c += 4) { + if (cells[c] !== EMPTY_CELL) throw new GlyphError(`row ${i + 1}, column ${c + 1}: a mark between lattice positions (a picture?)`, 'between', { row: i + 1, col: c + 1 }); + } + }); + const grid = rows.map((r) => [...r]); + const n = structure(rows); + const period = 4 * n - 1 + LINE_GAP; + const byShape = new Map(Object.values(vocab.parts).map((p) => [p.shape.join('/'), p.name])); + for (let top = 0; top < grid.length; top += period) { + const line = { bands: [] }; + for (let k = 0; k < n; k++) { + const r0 = top + 4 * k; + const band = grid.slice(r0, r0 + 3); + const marks = new Set(band.flat().filter((ch) => ch !== EMPTY_CELL)); + if (!marks.size) continue; + const where = `rows ${r0 + 1}-${r0 + 3}`; + if (marks.size > 1) throw new GlyphError(`${where}: one band mixes symbols ${[...marks].sort().join(' ')}`, 'mixed', { from: r0 + 1, to: r0 + 3 }); + const symbol = checkSymbol([...marks][0]); + const hs = []; + for (let pos = 0; pos < POSITIONS; pos++) { + const pattern = band.map((row) => row.slice(pos * 4, pos * 4 + 3).map((ch) => (ch !== EMPTY_CELL ? MARK : EMPTY_CELL)).join('')); + if (!pattern.some((p) => p.includes(MARK))) { hs.push(null); continue; } + const slot = Math.floor(pos / 2); + if (pos % 2 === 0) { + const name = byShape.get(pattern.join('/')); + if (name === undefined) throw new GlyphError(`${where}, position ${pos + 1}: not a part`, 'notPart', { from: r0 + 1, to: r0 + 3, pos: pos + 1 }); + hs.push(name); + } else { + const above = line.bands.length ? line.bands[0].words[slot] : null; + const kind = hs[hs.length - 1]; + let which; + if (kind === NUMBER || (kind === null && above !== null && (above.kind === NUMBER || above.which === NUMBER))) { + which = parseInt(pattern.join('').replaceAll(EMPTY_CELL, '0').replaceAll(MARK, '1'), 2); + } else { + which = byShape.get(pattern.join('/')) ?? null; + } + if (which === null) throw new GlyphError(`${where}, position ${pos + 1}: not a part or a number here`, 'notWhich', { from: r0 + 1, to: r0 + 3, pos: pos + 1 }); + hs.push(which); + } + } + line.bands.push({ symbol, words: [0, 2, 4].map((i) => word(hs[i], hs[i + 1])) }); + } + if (line.bands.length) page.lines.push(line); + } + return page; +} + +// ---------- reading a page as a knowledge graph (grammar rules 3 and 5) ---------- + +export function respond(above, below) { + if (below === null) return null; + if (above === null) return 'and also'; + if (below === above) return 'yes'; + if (below === 'NOT') return 'no'; + return 'instead'; +} + +export function nodeLabel(node, labels = LABELS) { + if (node.statement !== null) return node.statement ? labels.statement(node.statement) : labels.thatNothing; + return node.gloss; +} + +export function responseText(r, labels = LABELS) { + const tail = r.meaning === 'yes' || r.meaning === 'no' ? '' : labels.quote(r.part); + const to = r.to ? labels.to(r.to) : ''; + return `${r.slot}.${r.half}: ${labels[r.meaning]}${tail}${to}`; +} + +/** + * Rule 3: the first band of every line is one edge. Rule 5: every part in a lower band answers the + * nearest drawn part above it in the same position. + */ +export function readGraph(vocab, page) { + const g = { edges: [], nodes: {} }; + page.lines.forEach((line, idx) => { + const i = idx + 1; + const st = line.bands[0]; + const [subjectW, relationW, objectW] = st.words; + const node = (w) => { + if (sameWord(w, THAT)) return { word: null, gloss: '', statement: i - 1 }; + const gloss = vocab.gloss(w); + if (!isEmpty(w) && !(wordStr(w) in g.nodes)) g.nodes[wordStr(w)] = gloss; + return { word: isEmpty(w) ? null : wordStr(w), gloss, statement: null }; + }; + const subject = node(subjectW); + const relation = vocab.gloss(relationW, 'relation'); + const edge = { number: i, symbol: st.symbol, subject, relation, relationWord: wordStr(relationW), object: node(objectW), replies: [] }; + line.bands.slice(1).forEach((band, b0) => { + const b = b0 + 1; + const reply = { symbol: band.symbol, responses: [] }; + bandHalves(band).forEach((below, pos) => { + let above = null, by = null; + for (let j = b - 1; j >= 0; j--) { + const prev = bandHalves(line.bands[j])[pos]; + if (prev !== null) { above = prev; by = line.bands[j].symbol; break; } + } + const meaning = respond(above, below); + if (meaning) reply.responses.push({ slot: SLOTS[Math.floor(pos / 2)], half: HALVES[pos % 2], meaning, part: vocab.thing(below), to: by }); + }); + edge.replies.push(reply); + }); + g.edges.push(edge); + }); + return g; +} + +/** The graph as text lines, exactly as `glyph graph` prints it (in the vocabulary's own language). */ +export function formatGraph(vocab, g) { + const L = vocab.labels; + const out = [L.edges]; + for (const e of g.edges) { + out.push(`${e.number}. (${e.symbol}) ${nodeLabel(e.subject, L)} --${e.relation}--> ${nodeLabel(e.object, L)}`); + for (const r of e.replies) { + const text = r.responses.map((x) => responseText(x, L)).join('; ') || L.noComment; + out.push(` (${r.symbol}) ${text}`); + } + } + out.push('', L.nodes); + for (const [w, gloss] of Object.entries(g.nodes)) out.push(` ${w.padEnd(14)} ${gloss}`); + return out; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d410951 --- /dev/null +++ b/package.json @@ -0,0 +1 @@ +{"type":"module","private":true,"scripts":{"test":"node --test tests/*.test.mjs"}} diff --git a/script/convert/en/index.html b/script/convert/en/index.html new file mode 100644 index 0000000..8dc9dd1 --- /dev/null +++ b/script/convert/en/index.html @@ -0,0 +1,122 @@ + + + + + + +Grid script converter | The Contact Era + + + + + + + + + + + + + + +
    +
    +
    +

    Grid script · converter

    +

    Text to grid, grid to text

    +

    Write a page as text and see it drawn, or draw on the grid and read it back. Both ways also read the page as a graph, the way the translating machine does. Nothing leaves your browser.

    + +
    +
    + + + +
    +
    +
    +

    Text → drawing

    +

    One band per row: SYMBOL: node | relation | node. Words are KIND.WHICH from the dictionary, numbers are COUNT.n, and _ is empty. Rows under a statement are other voices answering it. A blank row starts the next statement. # starts a comment.

    +
    +
    +
    + + + +

    Symbols: + is them, × is us; any other single character is another voice.

    +
    +
    + +
    + + + + +
    +
    +
    The drawing as text
    +
    +
    +
    +
    + +
    +
    +
    +

    Drawing → text

    +

    Pick a part and click a position to stamp it, or pick "one cell" and draw freely. The lighter squares are the lattice: text only ever sits there, so a mark between them makes it a picture. You can also paste a drawing as text, one row per line, with . for empty cells.

    +
    +
    +
    +
    +
    + Draw as + + + +
    +
    +
    +
    + + + + +
    +
    +
    +
    + + +

    Read back as page source:

    +
    
    +          
    +
    +
    +
    +
    +
    +
    + + + + diff --git a/script/convert/index.html b/script/convert/index.html new file mode 100644 index 0000000..1da1428 --- /dev/null +++ b/script/convert/index.html @@ -0,0 +1,122 @@ + + + + + + +网格文字转换器 | 通联纪 + + + + + + + + + + + + + + +
    +
    +
    +

    网格文字 · 转换器

    +

    文字变图,图变文字

    +

    把一页写成文字,看它画出来;或者在网格上画,再把它读回来。两个方向都会像翻译机那样,把这一页读成一张图。所有内容都不会离开你的浏览器。

    + +
    +
    + + + +
    +
    +
    +

    文字 → 图

    +

    每行一个声部:符号: 节点 | 关系 | 节点。词写成词典里的 类.指,数写成 COUNT.n,_ 表示空。一句下面的行是其他声音在回应它。空一行开始下一句。# 后面是注释。

    +
    +
    +
    + + + +

    符号:+ 是他们,× 是我们;其他任何单个字符是另一个声音。

    +
    +
    + +
    + + + + +
    +
    +
    用文字表示的图
    +
    +
    +
    +
    + +
    +
    +
    +

    图 → 文字

    +

    选一个部件,点一个位置把它盖上去;或者选“单格”随手画。颜色浅一些的方块是格点:文字只会落在那里,画在它们之间的记号会让它变成一幅图。也可以粘贴用文字表示的图,每行一排,. 表示空格。

    +
    +
    +
    +
    +
    + 用谁的记号 + + + +
    +
    +
    +
    + + + + +
    +
    +
    +
    + + +

    读回的页面源文:

    +
    
    +          
    +
    +
    +
    +
    +
    +
    + + + + diff --git a/script/dictionary/en/index.html b/script/dictionary/en/index.html new file mode 100644 index 0000000..c575436 --- /dev/null +++ b/script/dictionary/en/index.html @@ -0,0 +1,72 @@ + + + + + + +Grid script dictionary | The Contact Era + + + + + + + + + + + + + + +
    +
    +
    +

    Grid script · dictionary

    +

    The lexicon

    +

    Every word the translating machine has agreed on so far, drawn as it would be sent. Search by meaning, or write a word out, like BODY.STAR or COUNT.42, to see how it draws and reads even if it is not here yet.

    + +
    +
    + + + +
    +
    +

    Words

    + + +
      +
      +
      + +
      +
      +
      +

      The sixteen parts

      +

      Every word is built from these. A part means a thing in a node and an action in a relation. A word that is not in the dictionary still reads as its two parts.

      +
      +
      + + +
      ShapePartAs a thingAs a relation
      +

      The same vocabulary ships with glyph, the command-line dictionary. How the parts combine is on the idea page.

      +
      +
      +
      + + + + diff --git a/script/dictionary/index.html b/script/dictionary/index.html new file mode 100644 index 0000000..59374fe --- /dev/null +++ b/script/dictionary/index.html @@ -0,0 +1,72 @@ + + + + + + +网格文字词典 | 通联纪 + + + + + + + + + + + + + + +
      +
      +
      +

      网格文字 · 词典

      +

      词库

      +

      翻译机到目前为止约定好的每一个词,按发送时的样子画出来。可以按意思搜索(中文或英文),也可以直接写出一个词,比如 BODY.STAR 或 COUNT.42,即使它还不在词典里,也能看到它怎么画、怎么读。

      + +
      +
      + + + +
      +
      +

      词

      + + +
        +
        +
        + +
        +
        +
        +

        十六个部件

        +

        每个词都由它们拼成。部件在节点里表示事物,在关系里表示动作。不在词典里的词,也能按它的两个部件读出来。

        +
        +
        + + +
        形状部件作为事物作为关系
        +

        同一份词表也随命令行词典 glyph 一起发布。部件怎样组合,见原理。

        +
        +
        +
        + + + + diff --git a/script/en/index.html b/script/en/index.html new file mode 100644 index 0000000..e842778 --- /dev/null +++ b/script/en/index.html @@ -0,0 +1,184 @@ + + + + + + +The grid script | The Contact Era + + + + + + + + + + + + + + +
        +
        +
        +

        Archive · the grid script

        +

        Writing to a voice we have never seen.

        +

        Once the beacon carried more than a picture, the translating machine needed a way to write it down. This is that script. It is our transcription of what Ross 128 b sends, not their own writing, and nobody knows what they look like. It grows from one idea about their speech: one voice alone can say only a little, and meaning grows when voices join.

        + + +
        +
        + + + +
        +
        +
        +

        1 · The grid

        +

        One grid, one mark per voice

        +
          +
        • Everything is drawn on one shared grid of cells.
        • +
        • Each party draws only with its own mark: + for Ross 128 b, × for us. Marks never cover each other, so you can always see who drew what.
        • +
        • One grid square of 3 × 3 cells is one part, and two parts make a word, the way strokes make a Chinese character. It is not an alphabet.
        • +
        • A line is one statement. Under it, other voices draw their answers.
        • +
        +

        So far, everything they have sent is drawn in + alone. The second band in the drawing is a reply humans could send: water, instead of air.

        +
        +
        +
        +
        + +
        +
        +
        +

        2 · Parts

        +

        Sixteen parts

        +

        Each part is a 3 × 3 shape with two meanings: one as a thing, and one (after the arrow) as a relation. The shapes are meant to be guessable: a closed square for us, a square opened toward someone for you, crossing rays for light, a rising staircase for sound.

        +
        +
          +
          +
          + +
          +
          +
          +

          3 · Words

          +

          Two parts make a word

          +

          A word takes two neighbouring positions: the kind on the left and the which on the right, written KIND.WHICH. A world that is yours is your world; a star that keeps time is a pulsar; air going is wind. A one-part word leaves the which empty. The dictionary lists every agreed word.

          +
          +
            +
            +
            + +
            +
            +
            +

            4 · Lines

            +

            Every line is one edge of a graph

            +

            A line is six positions wide (23 cells), so it holds three words: a node, a relation, another node. A text is a stack of lines, so a text is a knowledge graph. The same word on two lines is the same node, and a lone ONE in a node slot means "that": the statement on the line above.

            +
            +
            +
            +

            A relation is one part for the action, plus a which that marks time or polarity. With LIGHT (see):

            +
            +
            + + + + + + + + + + +
            WhichMeaningLIGHT …
            _no tensesee
            BEFOREpastsaw
            ONEnowsee now
            AFTERfuturewill see
            PATHalwayskeep seeing
            NOTnotdo not see
            OPENasked: does it?do … see?
            +
            +
            +
            +
            +
            +
            +

            The second example is the shadow transmission put into words: we saw you; we do not see you. The third points back at itself: your world is water, and we saw that.

            +
            +
            + +
            +
            +
            +

            5 · Numbers

            +

            Numbers are nine bits

            +

            A number is COUNT, a counting line, followed by a which read as nine bits: 256 down to 1, left to right, top to bottom. COUNT alone is zero, so 0 to 511 fit. COUNT is the only kind whose which is read as bits, so a number can never be mistaken for a word.

            +
            +
            +
            + +
            + + +

            Written COUNT.137. Try 137, the fifth transmission.

            +
            +
            +
            +
            +
            +
            +
            + +
            +
            +
            +

            6 · Voices

            +

            Stacking means another voice

            +

            Only a new party adds a band under a line, and each party keeps its band, so the number of bands is the number of voices. Every part in a lower band answers the nearest drawn part above it in the same position. That lets anyone agree with, deny or change any node or edge, part by part. A conversation is a graph several voices build together.

            +
            +
            + + + + + + + + +
            BelowMeans
            The same partyes
            NOTno
            A different partinstead
            A part under an empty positionand also
            Nothingno comment
            +
            +
            +
            +
            +
            +

            Questions: OPEN in any position asks "what goes here?", and whatever is drawn under it replaces it. In the last example a third voice, *, draws AIR under our WATER. It answers the nearest part above, which is ours: it sides with them, against us, about our own world.

            +
            +
            + +
            +
            +

            7 · The machine

            +

            How the machine reads a page

            +
              +
            1. Cut the page into 3 × 3 positions. Anything off the lattice is a picture, like the first 667-bit message, not text.
            2. +
            3. Split it into lines at the three-row gaps, and count the bands in a line: that is the number of voices.
            4. +
            5. Read the first band of each line as an edge: node, relation, node. Same word, same node. A lone ONE points at the line above.
            6. +
            7. Read every part in a lower band against the nearest part above it, and attach the answer to that node or edge, with the voice that drew it.
            8. +
            +

            Humans don't learn the signs; the machine reads and writes them, and its lexicon grows with every page. You can run the same machine: the converter turns text into drawings and drawings back into text, in your browser. It follows the same rules and the same vocabulary as glyph, the command-line dictionary (pip install glyph-cli).

            +
            +
            +
            + + + + diff --git a/script/index.html b/script/index.html new file mode 100644 index 0000000..1ddcd92 --- /dev/null +++ b/script/index.html @@ -0,0 +1,184 @@ + + + + + + +网格文字 | 通联纪 + + + + + + + + + + + + + + +
            +
            +
            +

            档案 · 网格文字

            +

            写给一个从未见过的声音。

            +

            信标带来的不再只是一幅图,翻译机就需要一种把它记下来的办法。这就是那套文字。它是我们对罗斯128 b 发来内容的转写,不是他们自己的书写;也没有人知道他们长什么样。它出自对他们说话方式的一个想法:一个声音单独只能说出很少的东西,声音汇合,意义才丰富起来。

            + + +
            +
            + + + +
            +
            +
            +

            1 · 网格

            +

            一张网格,每个声音一种记号

            +
              +
            • 一切都画在同一张共享的网格上。
            • +
            • 每一方只用自己的记号画:+ 是罗斯128 b,× 是我们。记号从不互相覆盖,所以总能看出是谁画的。
            • +
            • 一块 3 × 3 的格子是一个部件,两个部件组成一个词,就像笔画组成汉字。它不是字母表。
            • +
            • 一行是一句话。其他声音在它下面画出自己的回答。
            • +
            +

            到目前为止,他们发来的一切都只用 + 画成。图里的第二个声部是人类可以发回去的回答:是水,不是空气。

            +
            +
            +
            +
            + +
            +
            +
            +

            2 · 部件

            +

            十六个部件

            +

            每个部件是一个 3 × 3 的形状,有两种意思:作为事物的意思,和箭头后面作为关系的意思。形状尽量让人猜得出来:封闭的方块是我们,朝某人打开的方块是你们,交叉的光线是光,上升的台阶是声音。

            +
            +
              +
              +
              + +
              +
              +
              +

              3 · 词

              +

              两个部件组成一个词

              +

              一个词占两个相邻的位置:左边是类,右边是指,写作 类.指。属于你们的世界是你们的世界;计时的恒星是脉冲星;行进的空气是风。只有一个部件的词,右边空着。所有约定好的词都在词典里。

              +
              +
                +
                +
                + +
                +
                +
                +

                4 · 句

                +

                每一行是图谱的一条边

                +

                一行宽六个位置(23 格),正好放三个词:一个节点、一个关系、另一个节点。一段文字是一行行叠起来的,所以一段文字就是一张知识图谱。同一个词出现在两行,就是同一个节点;节点位置上单独一个 ONE 表示“那”,指上一行那句话。

                +
                +
                +
                +

                关系是一个表示动作的部件,加上一个标出时间或肯定否定的“指”。以 LIGHT(看见)为例:

                +
                +
                + + + + + + + + + + +
                指意思LIGHT …
                _无时态看见
                BEFORE过去看见过
                ONE现在正看见
                AFTER将来将会看见
                PATH一直一直看着
                NOT否定看不见
                OPEN问:是否?看见了吗?
                +
                +
                +
                +
                +
                +
                +

                第二个例子把“影子”那段发射写成了话:我们看见过你们;我们看不见你们了。第三个例子指向自己:你们的世界是水世界,我们看见过那个。

                +
                +
                + +
                +
                +
                +

                5 · 数

                +

                数是九个比特

                +

                一个数写作 COUNT(一条计数的横线),后面的“指”按九个比特读:从 256 到 1,从左到右,从上到下。COUNT 单独出现是零,所以能写 0 到 511。只有 COUNT 这一类的“指”按比特读,所以数永远不会被错认成词。

                +
                +
                +
                + +
                + + +

                写作 COUNT.137。试试 137,第五段发射。

                +
                +
                +
                +
                +
                +
                +
                + +
                +
                +
                +

                6 · 声音

                +

                往下叠,就是又一个声音

                +

                只有新的一方才会在一行下面加一个声部,而且每一方一直用同一个声部,所以声部的数目就是声音的数目。下面声部里的每个部件,回应同一位置上方最近的那个部件。这样任何一方都能逐个部件地赞同、否定或修改任何节点和边。一场对话,就是几个声音一起搭起来的一张图。

                +
                +
                + + + + + + + + +
                下面画的意思
                同一个部件是
                NOT不
                另一个部件换成
                空位置下面的部件还有
                什么都不画未表态
                +
                +
                +
                +
                +
                +

                提问:任何位置上的 OPEN 都在问“这里填什么?”,下面画的东西就取代它。最后一个例子里,第三个声音 * 在我们的 WATER 下面画了 AIR。它回应的是上方最近的部件,也就是我们的:它站在他们一边,反对我们对自己世界的说法。

                +
                +
                + +
                +
                +

                7 · 翻译机

                +

                翻译机怎样读一页

                +
                  +
                1. 把一页切成 3 × 3 的位置。不落在格点上的都是图,比如最早那幅 667 比特的图,而不是文字。
                2. +
                3. 在三行的空隙处分出句子,数一数一句有几个声部:那就是有几个声音。
                4. +
                5. 把每句的第一个声部读成一条边:节点、关系、节点。同一个词,同一个节点。单独的 ONE 指向上一行。
                6. +
                7. 把下面声部的每个部件和上方最近的部件对照,把回答连同画它的声音,挂到那个节点或那条边上。
                8. +
                +

                人类不用学这些符号;翻译机负责读和写,它的词库随着每一页增长。你也可以运行同一台机器:转换器在浏览器里把文字变成图,再把图变回文字。它和命令行词典 glyph(pip install glyph-cli)用的是同一套规则和同一份词表。

                +
                +
                +
                + + + + diff --git a/scripts/glyph_fixtures.py b/scripts/glyph_fixtures.py new file mode 100644 index 0000000..940b39c --- /dev/null +++ b/scripts/glyph_fixtures.py @@ -0,0 +1,53 @@ +"""Write tests/glyph-cli-fixtures.json: what glyph-cli itself says about the examples and the vocabulary. + +tests/glyph.test.mjs compares lib/glyph.js against this file, so the site and the `glyph` command agree. +Re-run it after updating glyph-cli or lib/glyph-vocab.json: + + pip install glyph-cli==0.2.0 + python3 scripts/glyph_fixtures.py +""" + +import json +from pathlib import Path + +import glyph_cli +from glyph_cli import GlyphError, Vocabulary +from glyph_cli.drawing import decode, draw_words, render, render_svg +from glyph_cli.graph import format_graph, read_graph +from glyph_cli.page import format_page, parse_page + +ROOT = Path(__file__).resolve().parent.parent +vocab = Vocabulary.from_json(json.loads((ROOT / "lib/glyph-vocab.json").read_text(encoding="utf-8"))) + +examples = {} +for path in sorted((ROOT / "tests/glyph-examples").glob("*.txt")): + source = path.read_text(encoding="utf-8") + page = parse_page(source, vocab) + rows = render(vocab, page) + examples[path.name] = { + "source": source, + "formatted": format_page(page), + "rows": rows, + "graph": format_graph(vocab, read_graph(vocab, page)), + "svg": render_svg(vocab, page), + "decoded": format_page(decode(vocab, "\n".join(rows))), + } + +words = {} +for e in vocab.entries: + words[str(e.word)] = {"rows": draw_words(vocab, [e.word]), "gloss": vocab.gloss(e.word)} + +parse = {} +for text in ["BODY.OTHER", "body.other", "SELF", "COUNT.137", "COUNT.0", "COUNT", "_.12", "_.WATER", "_", "", + "LIGHT.VOICE", "BODY.WIND", "COUNT.512", "STAR.12", "ONE.12", "COUNT.SELF", "FOO", "_.0"]: + try: + w = vocab.parse(text) + parse[text] = {"word": str(w), "node": vocab.gloss(w), "relation": vocab.gloss(w, role="relation")} + except GlyphError as exc: + parse[text] = {"error": str(exc)} + +numbers = {str(n): draw_words(vocab, [vocab.parse(f"COUNT.{n}")]) for n in range(0, 512)} + +out = {"glyph_cli": glyph_cli.__version__, "examples": examples, "words": words, "parse": parse, "numbers": numbers} +(ROOT / "tests/glyph-cli-fixtures.json").write_text(json.dumps(out, ensure_ascii=False, indent=1) + "\n", encoding="utf-8") +print(f"wrote tests/glyph-cli-fixtures.json from glyph-cli {glyph_cli.__version__}") diff --git a/tests/glyph-cli-fixtures.json b/tests/glyph-cli-fixtures.json new file mode 100644 index 0000000..6a6b156 --- /dev/null +++ b/tests/glyph-cli-fixtures.json @@ -0,0 +1,3325 @@ +{ + "glyph_cli": "0.2.0", + "examples": { + "conversation.txt": { + "source": "# They speak: three statements. A blank line starts the next edge.\n+: BODY.OTHER | ONE | BODY.AIR\n×: _ | _ | _.WATER\n*: _ | _ | _.AIR\n\n+: SELF | LIGHT.BEFORE | BODY.OTHER\n\n+: SELF | VOICE.ONE | ONE\n", + "formatted": "+: BODY.OTHER | ONE | BODY.AIR\n×: _ | _ | _.WATER\n*: _ | _ | _.AIR\n\n+: SELF | LIGHT.BEFORE | BODY.OTHER\n\n+: SELF | VOICE.ONE | ONE\n", + "rows": [ + "+++.+++.........+++.+.+", + "+++.+....+......+++....", + "+++.+++.........+++.+.+", + ".......................", + "....................×××", + ".......................", + "....................×××", + ".......................", + "....................*.*", + ".......................", + "....................*.*", + ".......................", + ".......................", + ".......................", + "+++.....+.+.....+++.+++", + "+.+......+..+...+++.+..", + "+++.....+.+.....+++.+++", + ".......................", + ".......................", + ".......................", + ".......................", + ".......................", + ".......................", + ".......................", + ".......................", + ".......................", + ".......................", + ".......................", + "+++.....+..............", + "+.+.....++...+...+.....", + "+++.....+++............", + ".......................", + ".......................", + ".......................", + ".......................", + ".......................", + ".......................", + ".......................", + "......................." + ], + "graph": [ + "Edges:", + "1. (+) your world --is, equals--> air-world", + " (×) object.which: instead \"water\" (to +)", + " (*) object.which: instead \"air\" (to ×)", + "2. (+) we --see [past]--> your world", + "3. (+) we --say, send [now]--> [statement 2]", + "", + "Nodes:", + " BODY.OTHER your world", + " BODY.AIR air-world", + " SELF we" + ], + "svg": "\n\n+\n+\n+\n\n+\n+\n+\n\n\n\n\n\n\n\n\n\n+\n+\n+\n\n+\n\n+\n+\n+\n+\n\n+\n\n\n\n\n+\n\n\n\n\n\n\n+\n+\n+\n\n\n\n\n+\n+\n+\n\n+\n+\n+\n\n\n\n\n\n\n\n\n\n+\n+\n+\n\n+\n\n+\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n×\n×\n×\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n×\n×\n×\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n*\n\n*\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n*\n\n*\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n+\n+\n+\n\n\n\n\n\n+\n\n+\n\n\n\n\n\n+\n+\n+\n\n+\n+\n+\n+\n\n+\n\n\n\n\n\n\n+\n\n\n+\n\n\n\n+\n+\n+\n\n+\n\n\n+\n+\n+\n\n\n\n\n\n+\n\n+\n\n\n\n\n\n+\n+\n+\n\n+\n+\n+\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n+\n+\n+\n\n\n\n\n\n+\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n+\n\n+\n\n\n\n\n\n+\n+\n\n\n\n+\n\n\n\n+\n\n\n\n\n\n+\n+\n+\n\n\n\n\n\n+\n+\n+\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", + "decoded": "+: BODY.OTHER | ONE | BODY.AIR\n×: _ | _ | _.WATER\n*: _ | _ | _.AIR\n\n+: SELF | LIGHT.BEFORE | BODY.OTHER\n\n+: SELF | VOICE.ONE | ONE\n" + }, + "how-many.txt": { + "source": "# \"How many pulsars?\" Twelve.\n# OPEN.COUNT asks \"what number?\"; the answer COUNT.12 replaces OPEN with COUNT, and COUNT with the bits.\n+: STAR.TIME | ONE | OPEN.COUNT\n×: _ | _ | COUNT.12\n", + "formatted": "+: STAR.TIME | ONE | OPEN.COUNT\n×: _ | _ | COUNT.12\n", + "rows": [ + ".+..+.+..........+.....", + "+++.+.+..+......+.+....", + ".+..+.+..........+..+++", + ".......................", + ".......................", + "......................×", + "................×××.×.." + ], + "graph": [ + "Edges:", + "1. (+) pulsar --is, equals--> how many?", + " (×) object.kind: instead \"number\" (to +); object.which: instead \"12\" (to +)", + "", + "Nodes:", + " STAR.TIME pulsar", + " OPEN.COUNT how many?" + ], + "svg": "\n\n\n+\n\n\n+\n\n+\n\n\n\n\n\n\n\n\n\n\n+\n\n\n\n\n\n+\n+\n+\n\n+\n\n+\n\n\n+\n\n\n\n\n\n\n+\n\n+\n\n\n\n\n\n+\n\n\n+\n\n+\n\n\n\n\n\n\n\n\n\n\n+\n\n\n+\n+\n+\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n×\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n×\n×\n×\n\n×\n\n\n\n", + "decoded": "+: STAR.TIME | ONE | OPEN.COUNT\n×: _ | _ | COUNT.12\n" + }, + "pulsars.txt": { + "source": "# \"We see pulsars. The pulsars are twelve.\"\n# The same word on two lines is one node with two edges.\n+: SELF | LIGHT.ONE | STAR.TIME\n\n+: STAR.TIME | ONE | COUNT.12\n", + "formatted": "+: SELF | LIGHT.ONE | STAR.TIME\n\n+: STAR.TIME | ONE | COUNT.12\n", + "rows": [ + "+++.....+.+......+..+.+", + "+.+......+...+..+++.+.+", + "+++.....+.+......+..+.+", + ".......................", + ".......................", + ".......................", + ".+..+.+................", + "+++.+.+..+............+", + ".+..+.+.........+++.+.." + ], + "graph": [ + "Edges:", + "1. (+) we --see [now]--> pulsar", + "2. (+) pulsar --is, equals--> 12", + "", + "Nodes:", + " SELF we", + " STAR.TIME pulsar", + " COUNT.12 12" + ], + "svg": "\n\n+\n+\n+\n\n\n\n\n\n+\n\n+\n\n\n\n\n\n\n+\n\n\n+\n\n+\n+\n\n+\n\n\n\n\n\n\n+\n\n\n\n+\n\n\n+\n+\n+\n\n+\n\n+\n+\n+\n+\n\n\n\n\n\n+\n\n+\n\n\n\n\n\n\n+\n\n\n+\n\n+\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n+\n\n\n+\n\n+\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n+\n+\n+\n\n+\n\n+\n\n\n+\n\n\n\n\n\n\n\n\n\n\n\n\n+\n\n+\n\n\n+\n\n+\n\n\n\n\n\n\n\n\n\n+\n+\n+\n\n+\n\n\n\n", + "decoded": "+: SELF | LIGHT.ONE | STAR.TIME\n\n+: STAR.TIME | ONE | COUNT.12\n" + }, + "question.txt": { + "source": "# \"Your world is what?\" and our answer: a water-world.\n+: BODY.OTHER | ONE | OPEN\n×: _ | _ | BODY.WATER\n", + "formatted": "+: BODY.OTHER | ONE | OPEN\n×: _ | _ | BODY.WATER\n", + "rows": [ + "+++.+++..........+.....", + "+++.+....+......+.+....", + "+++.+++..........+.....", + ".......................", + "................×××.×××", + "................×××....", + "................×××.×××" + ], + "graph": [ + "Edges:", + "1. (+) your world --is, equals--> what?", + " (×) object.kind: instead \"world, matter\" (to +); object.which: and also \"water\"", + "", + "Nodes:", + " BODY.OTHER your world", + " OPEN what?" + ], + "svg": "\n\n+\n+\n+\n\n+\n+\n+\n\n\n\n\n\n\n\n\n\n\n+\n\n\n\n\n\n+\n+\n+\n\n+\n\n\n\n\n+\n\n\n\n\n\n\n+\n\n+\n\n\n\n\n+\n+\n+\n\n+\n+\n+\n\n\n\n\n\n\n\n\n\n\n+\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n×\n×\n×\n\n×\n×\n×\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n×\n×\n×\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n×\n×\n×\n\n×\n×\n×\n\n", + "decoded": "+: BODY.OTHER | ONE | OPEN\n×: _ | _ | BODY.WATER\n" + }, + "saw-that.txt": { + "source": "# \"Your world is water. We saw that.\"\n# A lone ONE in a node slot is \"that\": the statement on the line above.\n+: BODY.OTHER | ONE | BODY.WATER\n\n+: SELF | LIGHT.BEFORE | ONE\n", + "formatted": "+: BODY.OTHER | ONE | BODY.WATER\n\n+: SELF | LIGHT.BEFORE | ONE\n", + "rows": [ + "+++.+++.........+++.+++", + "+++.+....+......+++....", + "+++.+++.........+++.+++", + ".......................", + ".......................", + ".......................", + "+++.....+.+............", + "+.+......+..+....+.....", + "+++.....+.+............" + ], + "graph": [ + "Edges:", + "1. (+) your world --is, equals--> water-world", + "2. (+) we --see [past]--> [statement 1]", + "", + "Nodes:", + " BODY.OTHER your world", + " BODY.WATER water-world", + " SELF we" + ], + "svg": "\n\n+\n+\n+\n\n+\n+\n+\n\n\n\n\n\n\n\n\n\n+\n+\n+\n\n+\n+\n+\n+\n+\n+\n\n+\n\n\n\n\n+\n\n\n\n\n\n\n+\n+\n+\n\n\n\n\n+\n+\n+\n\n+\n+\n+\n\n\n\n\n\n\n\n\n\n+\n+\n+\n\n+\n+\n+\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n+\n+\n+\n\n\n\n\n\n+\n\n+\n\n\n\n\n\n\n\n\n\n\n\n\n+\n\n+\n\n\n\n\n\n\n+\n\n\n+\n\n\n\n\n+\n\n\n\n\n\n+\n+\n+\n\n\n\n\n\n+\n\n+\n\n\n\n\n\n\n\n\n\n\n\n\n\n", + "decoded": "+: BODY.OTHER | ONE | BODY.WATER\n\n+: SELF | LIGHT.BEFORE | ONE\n" + }, + "saw-your-world.txt": { + "source": "# \"We saw your world.\"\n+: SELF | LIGHT.BEFORE | BODY.OTHER\n", + "formatted": "+: SELF | LIGHT.BEFORE | BODY.OTHER\n", + "rows": [ + "+++.....+.+.....+++.+++", + "+.+......+..+...+++.+..", + "+++.....+.+.....+++.+++" + ], + "graph": [ + "Edges:", + "1. (+) we --see [past]--> your world", + "", + "Nodes:", + " SELF we", + " BODY.OTHER your world" + ], + "svg": "\n\n+\n+\n+\n\n\n\n\n\n+\n\n+\n\n\n\n\n\n+\n+\n+\n\n+\n+\n+\n+\n\n+\n\n\n\n\n\n\n+\n\n\n+\n\n\n\n+\n+\n+\n\n+\n\n\n+\n+\n+\n\n\n\n\n\n+\n\n+\n\n\n\n\n\n+\n+\n+\n\n+\n+\n+\n\n", + "decoded": "+: SELF | LIGHT.BEFORE | BODY.OTHER\n" + }, + "shadow.txt": { + "source": "# \"We no longer see you.\" (the shadow transmission)\n# Change over time is two edges: we saw you; we do not see you.\n+: SELF | LIGHT.BEFORE | OTHER\n\n+: SELF | LIGHT.NOT | OTHER\n", + "formatted": "+: SELF | LIGHT.BEFORE | OTHER\n\n+: SELF | LIGHT.NOT | OTHER\n", + "rows": [ + "+++.....+.+.....+++....", + "+.+......+..+...+......", + "+++.....+.+.....+++....", + ".......................", + ".......................", + ".......................", + "+++.....+.+.+...+++....", + "+.+......+...+..+......", + "+++.....+.+...+.+++...." + ], + "graph": [ + "Edges:", + "1. (+) we --see [past]--> you", + "2. (+) we --see [not]--> you", + "", + "Nodes:", + " SELF we", + " OTHER you" + ], + "svg": "\n\n+\n+\n+\n\n\n\n\n\n+\n\n+\n\n\n\n\n\n+\n+\n+\n\n\n\n\n+\n\n+\n\n\n\n\n\n\n+\n\n\n+\n\n\n\n+\n\n\n\n\n\n\n+\n+\n+\n\n\n\n\n\n+\n\n+\n\n\n\n\n\n+\n+\n+\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n+\n+\n+\n\n\n\n\n\n+\n\n+\n\n+\n\n\n\n+\n+\n+\n\n\n\n\n+\n\n+\n\n\n\n\n\n\n+\n\n\n\n+\n\n\n+\n\n\n\n\n\n\n+\n+\n+\n\n\n\n\n\n+\n\n+\n\n\n\n+\n\n+\n+\n+\n\n\n\n\n\n", + "decoded": "+: SELF | LIGHT.BEFORE | OTHER\n\n+: SELF | LIGHT.NOT | OTHER\n" + }, + "third-voice.txt": { + "source": "# A third voice sides with them, against us, about our own world.\n+: BODY.OTHER | ONE | BODY.AIR\n×: _ | _ | _.WATER\n*: _ | _ | _.AIR\n", + "formatted": "+: BODY.OTHER | ONE | BODY.AIR\n×: _ | _ | _.WATER\n*: _ | _ | _.AIR\n", + "rows": [ + "+++.+++.........+++.+.+", + "+++.+....+......+++....", + "+++.+++.........+++.+.+", + ".......................", + "....................×××", + ".......................", + "....................×××", + ".......................", + "....................*.*", + ".......................", + "....................*.*" + ], + "graph": [ + "Edges:", + "1. (+) your world --is, equals--> air-world", + " (×) object.which: instead \"water\" (to +)", + " (*) object.which: instead \"air\" (to ×)", + "", + "Nodes:", + " BODY.OTHER your world", + " BODY.AIR air-world" + ], + "svg": "\n\n+\n+\n+\n\n+\n+\n+\n\n\n\n\n\n\n\n\n\n+\n+\n+\n\n+\n\n+\n+\n+\n+\n\n+\n\n\n\n\n+\n\n\n\n\n\n\n+\n+\n+\n\n\n\n\n+\n+\n+\n\n+\n+\n+\n\n\n\n\n\n\n\n\n\n+\n+\n+\n\n+\n\n+\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n×\n×\n×\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n×\n×\n×\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n*\n\n*\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n*\n\n*\n\n", + "decoded": "+: BODY.OTHER | ONE | BODY.AIR\n×: _ | _ | _.WATER\n*: _ | _ | _.AIR\n" + } + }, + "words": { + "SELF": { + "rows": [ + "+++....", + "+.+....", + "+++...." + ], + "gloss": "we" + }, + "OTHER": { + "rows": [ + "+++....", + "+......", + "+++...." + ], + "gloss": "you" + }, + "OTHER.OTHER": { + "rows": [ + "+++.+++", + "+...+..", + "+++.+++" + ], + "gloss": "a third party" + }, + "ONE.SELF": { + "rows": [ + "....+++", + ".+..+.+", + "....+++" + ], + "gloss": "one of us" + }, + "ONE.OTHER": { + "rows": [ + "....+++", + ".+..+..", + "....+++" + ], + "gloss": "one of you" + }, + "VOICE.SELF": { + "rows": [ + "+...+++", + "++..+.+", + "+++.+++" + ], + "gloss": "our voice" + }, + "VOICE.OTHER": { + "rows": [ + "+...+++", + "++..+..", + "+++.+++" + ], + "gloss": "your voice" + }, + "STAR": { + "rows": [ + ".+.....", + "+++....", + ".+....." + ], + "gloss": "star" + }, + "STAR.SELF": { + "rows": [ + ".+..+++", + "+++.+.+", + ".+..+++" + ], + "gloss": "our star" + }, + "STAR.OTHER": { + "rows": [ + ".+..+++", + "+++.+..", + ".+..+++" + ], + "gloss": "your star" + }, + "STAR.TIME": { + "rows": [ + ".+..+.+", + "+++.+.+", + ".+..+.+" + ], + "gloss": "pulsar" + }, + "STAR.STAR": { + "rows": [ + ".+...+.", + "+++.+++", + ".+...+." + ], + "gloss": "galaxy" + }, + "AIR.NOT": { + "rows": [ + "+.+.+..", + ".....+.", + "+.+...+" + ], + "gloss": "space" + }, + "LIGHT.PATH": { + "rows": [ + "+.+....", + ".+..+++", + "+.+...." + ], + "gloss": "ray, signal" + }, + "LIGHT.BODY": { + "rows": [ + "+.+.+++", + ".+..+++", + "+.+.+++" + ], + "gloss": "transit, shadow" + }, + "BODY": { + "rows": [ + "+++....", + "+++....", + "+++...." + ], + "gloss": "world, matter" + }, + "BODY.SELF": { + "rows": [ + "+++.+++", + "+++.+.+", + "+++.+++" + ], + "gloss": "our world" + }, + "BODY.OTHER": { + "rows": [ + "+++.+++", + "+++.+..", + "+++.+++" + ], + "gloss": "your world" + }, + "BODY.AIR": { + "rows": [ + "+++.+.+", + "+++....", + "+++.+.+" + ], + "gloss": "air-world" + }, + "BODY.WATER": { + "rows": [ + "+++.+++", + "+++....", + "+++.+++" + ], + "gloss": "water-world" + }, + "BODY.BODY": { + "rows": [ + "+++.+++", + "+++.+++", + "+++.+++" + ], + "gloss": "rock, ground" + }, + "BODY.PATH": { + "rows": [ + "+++....", + "+++.+++", + "+++...." + ], + "gloss": "orbit" + }, + "LIGHT": { + "rows": [ + "+.+....", + ".+.....", + "+.+...." + ], + "gloss": "light" + }, + "LIGHT.NOT": { + "rows": [ + "+.+.+..", + ".+...+.", + "+.+...+" + ], + "gloss": "dark, the night side" + }, + "PATH.LIGHT": { + "rows": [ + "....+.+", + "+++..+.", + "....+.+" + ], + "gloss": "terminator" + }, + "AIR": { + "rows": [ + "+.+....", + ".......", + "+.+...." + ], + "gloss": "air" + }, + "AIR.PATH": { + "rows": [ + "+.+....", + "....+++", + "+.+...." + ], + "gloss": "wind" + }, + "AIR.WATER": { + "rows": [ + "+.+.+++", + ".......", + "+.+.+++" + ], + "gloss": "cloud" + }, + "WATER": { + "rows": [ + "+++....", + ".......", + "+++...." + ], + "gloss": "water" + }, + "WATER.PATH": { + "rows": [ + "+++....", + "....+++", + "+++...." + ], + "gloss": "river" + }, + "WATER.BODY": { + "rows": [ + "+++.+++", + "....+++", + "+++.+++" + ], + "gloss": "ice" + }, + "WATER.NOT": { + "rows": [ + "+++.+..", + ".....+.", + "+++...+" + ], + "gloss": "dry land" + }, + "TIME": { + "rows": [ + "+.+....", + "+.+....", + "+.+...." + ], + "gloss": "time" + }, + "TIME.BEFORE": { + "rows": [ + "+.+....", + "+.+.+..", + "+.+...." + ], + "gloss": "the past, before" + }, + "TIME.ONE": { + "rows": [ + "+.+....", + "+.+..+.", + "+.+...." + ], + "gloss": "now" + }, + "TIME.AFTER": { + "rows": [ + "+.+....", + "+.+...+", + "+.+...." + ], + "gloss": "the future, after" + }, + "TIME.PATH": { + "rows": [ + "+.+....", + "+.+.+++", + "+.+...." + ], + "gloss": "always" + }, + "TIME.NOT": { + "rows": [ + "+.+.+..", + "+.+..+.", + "+.+...+" + ], + "gloss": "never" + }, + "TIME.BODY": { + "rows": [ + "+.+.+++", + "+.+.+++", + "+.+.+++" + ], + "gloss": "year" + }, + "TIME.VOICE": { + "rows": [ + "+.+.+..", + "+.+.++.", + "+.+.+++" + ], + "gloss": "rhythm, beat" + }, + "TIME.TIME": { + "rows": [ + "+.+.+.+", + "+.+.+.+", + "+.+.+.+" + ], + "gloss": "an age" + }, + "VOICE": { + "rows": [ + "+......", + "++.....", + "+++...." + ], + "gloss": "sound, voice" + }, + "VOICE.PATH": { + "rows": [ + "+......", + "++..+++", + "+++...." + ], + "gloss": "message" + }, + "VOICE.NOT": { + "rows": [ + "+...+..", + "++...+.", + "+++...+" + ], + "gloss": "silence" + }, + "VOICE.VOICE": { + "rows": [ + "+...+..", + "++..++.", + "+++.+++" + ], + "gloss": "conversation" + }, + "VOICE.ONE": { + "rows": [ + "+......", + "++...+.", + "+++...." + ], + "gloss": "monologue" + }, + "VOICE.AIR": { + "rows": [ + "+...+.+", + "++.....", + "+++.+.+" + ], + "gloss": "speech" + }, + "VOICE.LIGHT": { + "rows": [ + "+...+.+", + "++...+.", + "+++.+.+" + ], + "gloss": "radio" + }, + "VOICE.BODY": { + "rows": [ + "+...+++", + "++..+++", + "+++.+++" + ], + "gloss": "resonator, instrument" + }, + "VOICE.OPEN": { + "rows": [ + "+....+.", + "++..+.+", + "+++..+." + ], + "gloss": "a question" + }, + "ONE": { + "rows": [ + ".......", + ".+.....", + "......." + ], + "gloss": "that" + }, + "ONE.ONE": { + "rows": [ + ".......", + ".+...+.", + "......." + ], + "gloss": "the same" + }, + "ONE.NOT": { + "rows": [ + "....+..", + ".+...+.", + "......+" + ], + "gloss": "different" + }, + "OPEN.COUNT": { + "rows": [ + ".+.....", + "+.+....", + ".+..+++" + ], + "gloss": "how many?" + }, + "OPEN": { + "rows": [ + ".+.....", + "+.+....", + ".+....." + ], + "gloss": "what?" + } + }, + "parse": { + "BODY.OTHER": { + "word": "BODY.OTHER", + "node": "your world", + "relation": "have, contain [OTHER]" + }, + "body.other": { + "word": "BODY.OTHER", + "node": "your world", + "relation": "have, contain [OTHER]" + }, + "SELF": { + "word": "SELF", + "node": "we", + "relation": "know" + }, + "COUNT.137": { + "word": "COUNT.137", + "node": "137", + "relation": "number [137]" + }, + "COUNT.0": { + "word": "COUNT", + "node": "0", + "relation": "number" + }, + "COUNT": { + "word": "COUNT", + "node": "0", + "relation": "number" + }, + "_.12": { + "word": "_.12", + "node": "_ | 12 (not in vocabulary)", + "relation": " [12]" + }, + "_.WATER": { + "word": "_.WATER", + "node": "_ | water (not in vocabulary)", + "relation": " [WATER]" + }, + "_": { + "word": "_", + "node": "—", + "relation": "—" + }, + "": { + "word": "_", + "node": "—", + "relation": "—" + }, + "LIGHT.VOICE": { + "word": "LIGHT.VOICE", + "node": "light | sound (not in vocabulary)", + "relation": "see [VOICE]" + }, + "BODY.WIND": { + "error": "unknown part: WIND (see `glyph parts`)" + }, + "COUNT.512": { + "error": "number out of range 0-511: 512" + }, + "STAR.12": { + "error": "a number needs COUNT as its kind, e.g. COUNT.12 (got STAR.12)" + }, + "ONE.12": { + "error": "a number needs COUNT as its kind, e.g. COUNT.12 (got ONE.12)" + }, + "COUNT.SELF": { + "error": "COUNT only takes a number as its which, e.g. COUNT.12 (got COUNT.SELF)" + }, + "FOO": { + "error": "unknown part: FOO (see `glyph parts`)" + }, + "_.0": { + "word": "_.0", + "node": "_ | 0 (not in vocabulary)", + "relation": " [0]" + } + }, + "numbers": { + "0": [ + ".......", + ".......", + "+++...." + ], + "1": [ + ".......", + ".......", + "+++...+" + ], + "2": [ + ".......", + ".......", + "+++..+." + ], + "3": [ + ".......", + ".......", + "+++..++" + ], + "4": [ + ".......", + ".......", + "+++.+.." + ], + "5": [ + ".......", + ".......", + "+++.+.+" + ], + "6": [ + ".......", + ".......", + "+++.++." + ], + "7": [ + ".......", + ".......", + "+++.+++" + ], + "8": [ + ".......", + "......+", + "+++...." + ], + "9": [ + ".......", + "......+", + "+++...+" + ], + "10": [ + ".......", + "......+", + "+++..+." + ], + "11": [ + ".......", + "......+", + "+++..++" + ], + "12": [ + ".......", + "......+", + "+++.+.." + ], + "13": [ + ".......", + "......+", + "+++.+.+" + ], + "14": [ + ".......", + "......+", + "+++.++." + ], + "15": [ + ".......", + "......+", + "+++.+++" + ], + "16": [ + ".......", + ".....+.", + "+++...." + ], + "17": [ + ".......", + ".....+.", + "+++...+" + ], + "18": [ + ".......", + ".....+.", + "+++..+." + ], + "19": [ + ".......", + ".....+.", + "+++..++" + ], + "20": [ + ".......", + ".....+.", + "+++.+.." + ], + "21": [ + ".......", + ".....+.", + "+++.+.+" + ], + "22": [ + ".......", + ".....+.", + "+++.++." + ], + "23": [ + ".......", + ".....+.", + "+++.+++" + ], + "24": [ + ".......", + ".....++", + "+++...." + ], + "25": [ + ".......", + ".....++", + "+++...+" + ], + "26": [ + ".......", + ".....++", + "+++..+." + ], + "27": [ + ".......", + ".....++", + "+++..++" + ], + "28": [ + ".......", + ".....++", + "+++.+.." + ], + "29": [ + ".......", + ".....++", + "+++.+.+" + ], + "30": [ + ".......", + ".....++", + "+++.++." + ], + "31": [ + ".......", + ".....++", + "+++.+++" + ], + "32": [ + ".......", + "....+..", + "+++...." + ], + "33": [ + ".......", + "....+..", + "+++...+" + ], + "34": [ + ".......", + "....+..", + "+++..+." + ], + "35": [ + ".......", + "....+..", + "+++..++" + ], + "36": [ + ".......", + "....+..", + "+++.+.." + ], + "37": [ + ".......", + "....+..", + "+++.+.+" + ], + "38": [ + ".......", + "....+..", + "+++.++." + ], + "39": [ + ".......", + "....+..", + "+++.+++" + ], + "40": [ + ".......", + "....+.+", + "+++...." + ], + "41": [ + ".......", + "....+.+", + "+++...+" + ], + "42": [ + ".......", + "....+.+", + "+++..+." + ], + "43": [ + ".......", + "....+.+", + "+++..++" + ], + "44": [ + ".......", + "....+.+", + "+++.+.." + ], + "45": [ + ".......", + "....+.+", + "+++.+.+" + ], + "46": [ + ".......", + "....+.+", + "+++.++." + ], + "47": [ + ".......", + "....+.+", + "+++.+++" + ], + "48": [ + ".......", + "....++.", + "+++...." + ], + "49": [ + ".......", + "....++.", + "+++...+" + ], + "50": [ + ".......", + "....++.", + "+++..+." + ], + "51": [ + ".......", + "....++.", + "+++..++" + ], + "52": [ + ".......", + "....++.", + "+++.+.." + ], + "53": [ + ".......", + "....++.", + "+++.+.+" + ], + "54": [ + ".......", + "....++.", + "+++.++." + ], + "55": [ + ".......", + "....++.", + "+++.+++" + ], + "56": [ + ".......", + "....+++", + "+++...." + ], + "57": [ + ".......", + "....+++", + "+++...+" + ], + "58": [ + ".......", + "....+++", + "+++..+." + ], + "59": [ + ".......", + "....+++", + "+++..++" + ], + "60": [ + ".......", + "....+++", + "+++.+.." + ], + "61": [ + ".......", + "....+++", + "+++.+.+" + ], + "62": [ + ".......", + "....+++", + "+++.++." + ], + "63": [ + ".......", + "....+++", + "+++.+++" + ], + "64": [ + "......+", + ".......", + "+++...." + ], + "65": [ + "......+", + ".......", + "+++...+" + ], + "66": [ + "......+", + ".......", + "+++..+." + ], + "67": [ + "......+", + ".......", + "+++..++" + ], + "68": [ + "......+", + ".......", + "+++.+.." + ], + "69": [ + "......+", + ".......", + "+++.+.+" + ], + "70": [ + "......+", + ".......", + "+++.++." + ], + "71": [ + "......+", + ".......", + "+++.+++" + ], + "72": [ + "......+", + "......+", + "+++...." + ], + "73": [ + "......+", + "......+", + "+++...+" + ], + "74": [ + "......+", + "......+", + "+++..+." + ], + "75": [ + "......+", + "......+", + "+++..++" + ], + "76": [ + "......+", + "......+", + "+++.+.." + ], + "77": [ + "......+", + "......+", + "+++.+.+" + ], + "78": [ + "......+", + "......+", + "+++.++." + ], + "79": [ + "......+", + "......+", + "+++.+++" + ], + "80": [ + "......+", + ".....+.", + "+++...." + ], + "81": [ + "......+", + ".....+.", + "+++...+" + ], + "82": [ + "......+", + ".....+.", + "+++..+." + ], + "83": [ + "......+", + ".....+.", + "+++..++" + ], + "84": [ + "......+", + ".....+.", + "+++.+.." + ], + "85": [ + "......+", + ".....+.", + "+++.+.+" + ], + "86": [ + "......+", + ".....+.", + "+++.++." + ], + "87": [ + "......+", + ".....+.", + "+++.+++" + ], + "88": [ + "......+", + ".....++", + "+++...." + ], + "89": [ + "......+", + ".....++", + "+++...+" + ], + "90": [ + "......+", + ".....++", + "+++..+." + ], + "91": [ + "......+", + ".....++", + "+++..++" + ], + "92": [ + "......+", + ".....++", + "+++.+.." + ], + "93": [ + "......+", + ".....++", + "+++.+.+" + ], + "94": [ + "......+", + ".....++", + "+++.++." + ], + "95": [ + "......+", + ".....++", + "+++.+++" + ], + "96": [ + "......+", + "....+..", + "+++...." + ], + "97": [ + "......+", + "....+..", + "+++...+" + ], + "98": [ + "......+", + "....+..", + "+++..+." + ], + "99": [ + "......+", + "....+..", + "+++..++" + ], + "100": [ + "......+", + "....+..", + "+++.+.." + ], + "101": [ + "......+", + "....+..", + "+++.+.+" + ], + "102": [ + "......+", + "....+..", + "+++.++." + ], + "103": [ + "......+", + "....+..", + "+++.+++" + ], + "104": [ + "......+", + "....+.+", + "+++...." + ], + "105": [ + "......+", + "....+.+", + "+++...+" + ], + "106": [ + "......+", + "....+.+", + "+++..+." + ], + "107": [ + "......+", + "....+.+", + "+++..++" + ], + "108": [ + "......+", + "....+.+", + "+++.+.." + ], + "109": [ + "......+", + "....+.+", + "+++.+.+" + ], + "110": [ + "......+", + "....+.+", + "+++.++." + ], + "111": [ + "......+", + "....+.+", + "+++.+++" + ], + "112": [ + "......+", + "....++.", + "+++...." + ], + "113": [ + "......+", + "....++.", + "+++...+" + ], + "114": [ + "......+", + "....++.", + "+++..+." + ], + "115": [ + "......+", + "....++.", + "+++..++" + ], + "116": [ + "......+", + "....++.", + "+++.+.." + ], + "117": [ + "......+", + "....++.", + "+++.+.+" + ], + "118": [ + "......+", + "....++.", + "+++.++." + ], + "119": [ + "......+", + "....++.", + "+++.+++" + ], + "120": [ + "......+", + "....+++", + "+++...." + ], + "121": [ + "......+", + "....+++", + "+++...+" + ], + "122": [ + "......+", + "....+++", + "+++..+." + ], + "123": [ + "......+", + "....+++", + "+++..++" + ], + "124": [ + "......+", + "....+++", + "+++.+.." + ], + "125": [ + "......+", + "....+++", + "+++.+.+" + ], + "126": [ + "......+", + "....+++", + "+++.++." + ], + "127": [ + "......+", + "....+++", + "+++.+++" + ], + "128": [ + ".....+.", + ".......", + "+++...." + ], + "129": [ + ".....+.", + ".......", + "+++...+" + ], + "130": [ + ".....+.", + ".......", + "+++..+." + ], + "131": [ + ".....+.", + ".......", + "+++..++" + ], + "132": [ + ".....+.", + ".......", + "+++.+.." + ], + "133": [ + ".....+.", + ".......", + "+++.+.+" + ], + "134": [ + ".....+.", + ".......", + "+++.++." + ], + "135": [ + ".....+.", + ".......", + "+++.+++" + ], + "136": [ + ".....+.", + "......+", + "+++...." + ], + "137": [ + ".....+.", + "......+", + "+++...+" + ], + "138": [ + ".....+.", + "......+", + "+++..+." + ], + "139": [ + ".....+.", + "......+", + "+++..++" + ], + "140": [ + ".....+.", + "......+", + "+++.+.." + ], + "141": [ + ".....+.", + "......+", + "+++.+.+" + ], + "142": [ + ".....+.", + "......+", + "+++.++." + ], + "143": [ + ".....+.", + "......+", + "+++.+++" + ], + "144": [ + ".....+.", + ".....+.", + "+++...." + ], + "145": [ + ".....+.", + ".....+.", + "+++...+" + ], + "146": [ + ".....+.", + ".....+.", + "+++..+." + ], + "147": [ + ".....+.", + ".....+.", + "+++..++" + ], + "148": [ + ".....+.", + ".....+.", + "+++.+.." + ], + "149": [ + ".....+.", + ".....+.", + "+++.+.+" + ], + "150": [ + ".....+.", + ".....+.", + "+++.++." + ], + "151": [ + ".....+.", + ".....+.", + "+++.+++" + ], + "152": [ + ".....+.", + ".....++", + "+++...." + ], + "153": [ + ".....+.", + ".....++", + "+++...+" + ], + "154": [ + ".....+.", + ".....++", + "+++..+." + ], + "155": [ + ".....+.", + ".....++", + "+++..++" + ], + "156": [ + ".....+.", + ".....++", + "+++.+.." + ], + "157": [ + ".....+.", + ".....++", + "+++.+.+" + ], + "158": [ + ".....+.", + ".....++", + "+++.++." + ], + "159": [ + ".....+.", + ".....++", + "+++.+++" + ], + "160": [ + ".....+.", + "....+..", + "+++...." + ], + "161": [ + ".....+.", + "....+..", + "+++...+" + ], + "162": [ + ".....+.", + "....+..", + "+++..+." + ], + "163": [ + ".....+.", + "....+..", + "+++..++" + ], + "164": [ + ".....+.", + "....+..", + "+++.+.." + ], + "165": [ + ".....+.", + "....+..", + "+++.+.+" + ], + "166": [ + ".....+.", + "....+..", + "+++.++." + ], + "167": [ + ".....+.", + "....+..", + "+++.+++" + ], + "168": [ + ".....+.", + "....+.+", + "+++...." + ], + "169": [ + ".....+.", + "....+.+", + "+++...+" + ], + "170": [ + ".....+.", + "....+.+", + "+++..+." + ], + "171": [ + ".....+.", + "....+.+", + "+++..++" + ], + "172": [ + ".....+.", + "....+.+", + "+++.+.." + ], + "173": [ + ".....+.", + "....+.+", + "+++.+.+" + ], + "174": [ + ".....+.", + "....+.+", + "+++.++." + ], + "175": [ + ".....+.", + "....+.+", + "+++.+++" + ], + "176": [ + ".....+.", + "....++.", + "+++...." + ], + "177": [ + ".....+.", + "....++.", + "+++...+" + ], + "178": [ + ".....+.", + "....++.", + "+++..+." + ], + "179": [ + ".....+.", + "....++.", + "+++..++" + ], + "180": [ + ".....+.", + "....++.", + "+++.+.." + ], + "181": [ + ".....+.", + "....++.", + "+++.+.+" + ], + "182": [ + ".....+.", + "....++.", + "+++.++." + ], + "183": [ + ".....+.", + "....++.", + "+++.+++" + ], + "184": [ + ".....+.", + "....+++", + "+++...." + ], + "185": [ + ".....+.", + "....+++", + "+++...+" + ], + "186": [ + ".....+.", + "....+++", + "+++..+." + ], + "187": [ + ".....+.", + "....+++", + "+++..++" + ], + "188": [ + ".....+.", + "....+++", + "+++.+.." + ], + "189": [ + ".....+.", + "....+++", + "+++.+.+" + ], + "190": [ + ".....+.", + "....+++", + "+++.++." + ], + "191": [ + ".....+.", + "....+++", + "+++.+++" + ], + "192": [ + ".....++", + ".......", + "+++...." + ], + "193": [ + ".....++", + ".......", + "+++...+" + ], + "194": [ + ".....++", + ".......", + "+++..+." + ], + "195": [ + ".....++", + ".......", + "+++..++" + ], + "196": [ + ".....++", + ".......", + "+++.+.." + ], + "197": [ + ".....++", + ".......", + "+++.+.+" + ], + "198": [ + ".....++", + ".......", + "+++.++." + ], + "199": [ + ".....++", + ".......", + "+++.+++" + ], + "200": [ + ".....++", + "......+", + "+++...." + ], + "201": [ + ".....++", + "......+", + "+++...+" + ], + "202": [ + ".....++", + "......+", + "+++..+." + ], + "203": [ + ".....++", + "......+", + "+++..++" + ], + "204": [ + ".....++", + "......+", + "+++.+.." + ], + "205": [ + ".....++", + "......+", + "+++.+.+" + ], + "206": [ + ".....++", + "......+", + "+++.++." + ], + "207": [ + ".....++", + "......+", + "+++.+++" + ], + "208": [ + ".....++", + ".....+.", + "+++...." + ], + "209": [ + ".....++", + ".....+.", + "+++...+" + ], + "210": [ + ".....++", + ".....+.", + "+++..+." + ], + "211": [ + ".....++", + ".....+.", + "+++..++" + ], + "212": [ + ".....++", + ".....+.", + "+++.+.." + ], + "213": [ + ".....++", + ".....+.", + "+++.+.+" + ], + "214": [ + ".....++", + ".....+.", + "+++.++." + ], + "215": [ + ".....++", + ".....+.", + "+++.+++" + ], + "216": [ + ".....++", + ".....++", + "+++...." + ], + "217": [ + ".....++", + ".....++", + "+++...+" + ], + "218": [ + ".....++", + ".....++", + "+++..+." + ], + "219": [ + ".....++", + ".....++", + "+++..++" + ], + "220": [ + ".....++", + ".....++", + "+++.+.." + ], + "221": [ + ".....++", + ".....++", + "+++.+.+" + ], + "222": [ + ".....++", + ".....++", + "+++.++." + ], + "223": [ + ".....++", + ".....++", + "+++.+++" + ], + "224": [ + ".....++", + "....+..", + "+++...." + ], + "225": [ + ".....++", + "....+..", + "+++...+" + ], + "226": [ + ".....++", + "....+..", + "+++..+." + ], + "227": [ + ".....++", + "....+..", + "+++..++" + ], + "228": [ + ".....++", + "....+..", + "+++.+.." + ], + "229": [ + ".....++", + "....+..", + "+++.+.+" + ], + "230": [ + ".....++", + "....+..", + "+++.++." + ], + "231": [ + ".....++", + "....+..", + "+++.+++" + ], + "232": [ + ".....++", + "....+.+", + "+++...." + ], + "233": [ + ".....++", + "....+.+", + "+++...+" + ], + "234": [ + ".....++", + "....+.+", + "+++..+." + ], + "235": [ + ".....++", + "....+.+", + "+++..++" + ], + "236": [ + ".....++", + "....+.+", + "+++.+.." + ], + "237": [ + ".....++", + "....+.+", + "+++.+.+" + ], + "238": [ + ".....++", + "....+.+", + "+++.++." + ], + "239": [ + ".....++", + "....+.+", + "+++.+++" + ], + "240": [ + ".....++", + "....++.", + "+++...." + ], + "241": [ + ".....++", + "....++.", + "+++...+" + ], + "242": [ + ".....++", + "....++.", + "+++..+." + ], + "243": [ + ".....++", + "....++.", + "+++..++" + ], + "244": [ + ".....++", + "....++.", + "+++.+.." + ], + "245": [ + ".....++", + "....++.", + "+++.+.+" + ], + "246": [ + ".....++", + "....++.", + "+++.++." + ], + "247": [ + ".....++", + "....++.", + "+++.+++" + ], + "248": [ + ".....++", + "....+++", + "+++...." + ], + "249": [ + ".....++", + "....+++", + "+++...+" + ], + "250": [ + ".....++", + "....+++", + "+++..+." + ], + "251": [ + ".....++", + "....+++", + "+++..++" + ], + "252": [ + ".....++", + "....+++", + "+++.+.." + ], + "253": [ + ".....++", + "....+++", + "+++.+.+" + ], + "254": [ + ".....++", + "....+++", + "+++.++." + ], + "255": [ + ".....++", + "....+++", + "+++.+++" + ], + "256": [ + "....+..", + ".......", + "+++...." + ], + "257": [ + "....+..", + ".......", + "+++...+" + ], + "258": [ + "....+..", + ".......", + "+++..+." + ], + "259": [ + "....+..", + ".......", + "+++..++" + ], + "260": [ + "....+..", + ".......", + "+++.+.." + ], + "261": [ + "....+..", + ".......", + "+++.+.+" + ], + "262": [ + "....+..", + ".......", + "+++.++." + ], + "263": [ + "....+..", + ".......", + "+++.+++" + ], + "264": [ + "....+..", + "......+", + "+++...." + ], + "265": [ + "....+..", + "......+", + "+++...+" + ], + "266": [ + "....+..", + "......+", + "+++..+." + ], + "267": [ + "....+..", + "......+", + "+++..++" + ], + "268": [ + "....+..", + "......+", + "+++.+.." + ], + "269": [ + "....+..", + "......+", + "+++.+.+" + ], + "270": [ + "....+..", + "......+", + "+++.++." + ], + "271": [ + "....+..", + "......+", + "+++.+++" + ], + "272": [ + "....+..", + ".....+.", + "+++...." + ], + "273": [ + "....+..", + ".....+.", + "+++...+" + ], + "274": [ + "....+..", + ".....+.", + "+++..+." + ], + "275": [ + "....+..", + ".....+.", + "+++..++" + ], + "276": [ + "....+..", + ".....+.", + "+++.+.." + ], + "277": [ + "....+..", + ".....+.", + "+++.+.+" + ], + "278": [ + "....+..", + ".....+.", + "+++.++." + ], + "279": [ + "....+..", + ".....+.", + "+++.+++" + ], + "280": [ + "....+..", + ".....++", + "+++...." + ], + "281": [ + "....+..", + ".....++", + "+++...+" + ], + "282": [ + "....+..", + ".....++", + "+++..+." + ], + "283": [ + "....+..", + ".....++", + "+++..++" + ], + "284": [ + "....+..", + ".....++", + "+++.+.." + ], + "285": [ + "....+..", + ".....++", + "+++.+.+" + ], + "286": [ + "....+..", + ".....++", + "+++.++." + ], + "287": [ + "....+..", + ".....++", + "+++.+++" + ], + "288": [ + "....+..", + "....+..", + "+++...." + ], + "289": [ + "....+..", + "....+..", + "+++...+" + ], + "290": [ + "....+..", + "....+..", + "+++..+." + ], + "291": [ + "....+..", + "....+..", + "+++..++" + ], + "292": [ + "....+..", + "....+..", + "+++.+.." + ], + "293": [ + "....+..", + "....+..", + "+++.+.+" + ], + "294": [ + "....+..", + "....+..", + "+++.++." + ], + "295": [ + "....+..", + "....+..", + "+++.+++" + ], + "296": [ + "....+..", + "....+.+", + "+++...." + ], + "297": [ + "....+..", + "....+.+", + "+++...+" + ], + "298": [ + "....+..", + "....+.+", + "+++..+." + ], + "299": [ + "....+..", + "....+.+", + "+++..++" + ], + "300": [ + "....+..", + "....+.+", + "+++.+.." + ], + "301": [ + "....+..", + "....+.+", + "+++.+.+" + ], + "302": [ + "....+..", + "....+.+", + "+++.++." + ], + "303": [ + "....+..", + "....+.+", + "+++.+++" + ], + "304": [ + "....+..", + "....++.", + "+++...." + ], + "305": [ + "....+..", + "....++.", + "+++...+" + ], + "306": [ + "....+..", + "....++.", + "+++..+." + ], + "307": [ + "....+..", + "....++.", + "+++..++" + ], + "308": [ + "....+..", + "....++.", + "+++.+.." + ], + "309": [ + "....+..", + "....++.", + "+++.+.+" + ], + "310": [ + "....+..", + "....++.", + "+++.++." + ], + "311": [ + "....+..", + "....++.", + "+++.+++" + ], + "312": [ + "....+..", + "....+++", + "+++...." + ], + "313": [ + "....+..", + "....+++", + "+++...+" + ], + "314": [ + "....+..", + "....+++", + "+++..+." + ], + "315": [ + "....+..", + "....+++", + "+++..++" + ], + "316": [ + "....+..", + "....+++", + "+++.+.." + ], + "317": [ + "....+..", + "....+++", + "+++.+.+" + ], + "318": [ + "....+..", + "....+++", + "+++.++." + ], + "319": [ + "....+..", + "....+++", + "+++.+++" + ], + "320": [ + "....+.+", + ".......", + "+++...." + ], + "321": [ + "....+.+", + ".......", + "+++...+" + ], + "322": [ + "....+.+", + ".......", + "+++..+." + ], + "323": [ + "....+.+", + ".......", + "+++..++" + ], + "324": [ + "....+.+", + ".......", + "+++.+.." + ], + "325": [ + "....+.+", + ".......", + "+++.+.+" + ], + "326": [ + "....+.+", + ".......", + "+++.++." + ], + "327": [ + "....+.+", + ".......", + "+++.+++" + ], + "328": [ + "....+.+", + "......+", + "+++...." + ], + "329": [ + "....+.+", + "......+", + "+++...+" + ], + "330": [ + "....+.+", + "......+", + "+++..+." + ], + "331": [ + "....+.+", + "......+", + "+++..++" + ], + "332": [ + "....+.+", + "......+", + "+++.+.." + ], + "333": [ + "....+.+", + "......+", + "+++.+.+" + ], + "334": [ + "....+.+", + "......+", + "+++.++." + ], + "335": [ + "....+.+", + "......+", + "+++.+++" + ], + "336": [ + "....+.+", + ".....+.", + "+++...." + ], + "337": [ + "....+.+", + ".....+.", + "+++...+" + ], + "338": [ + "....+.+", + ".....+.", + "+++..+." + ], + "339": [ + "....+.+", + ".....+.", + "+++..++" + ], + "340": [ + "....+.+", + ".....+.", + "+++.+.." + ], + "341": [ + "....+.+", + ".....+.", + "+++.+.+" + ], + "342": [ + "....+.+", + ".....+.", + "+++.++." + ], + "343": [ + "....+.+", + ".....+.", + "+++.+++" + ], + "344": [ + "....+.+", + ".....++", + "+++...." + ], + "345": [ + "....+.+", + ".....++", + "+++...+" + ], + "346": [ + "....+.+", + ".....++", + "+++..+." + ], + "347": [ + "....+.+", + ".....++", + "+++..++" + ], + "348": [ + "....+.+", + ".....++", + "+++.+.." + ], + "349": [ + "....+.+", + ".....++", + "+++.+.+" + ], + "350": [ + "....+.+", + ".....++", + "+++.++." + ], + "351": [ + "....+.+", + ".....++", + "+++.+++" + ], + "352": [ + "....+.+", + "....+..", + "+++...." + ], + "353": [ + "....+.+", + "....+..", + "+++...+" + ], + "354": [ + "....+.+", + "....+..", + "+++..+." + ], + "355": [ + "....+.+", + "....+..", + "+++..++" + ], + "356": [ + "....+.+", + "....+..", + "+++.+.." + ], + "357": [ + "....+.+", + "....+..", + "+++.+.+" + ], + "358": [ + "....+.+", + "....+..", + "+++.++." + ], + "359": [ + "....+.+", + "....+..", + "+++.+++" + ], + "360": [ + "....+.+", + "....+.+", + "+++...." + ], + "361": [ + "....+.+", + "....+.+", + "+++...+" + ], + "362": [ + "....+.+", + "....+.+", + "+++..+." + ], + "363": [ + "....+.+", + "....+.+", + "+++..++" + ], + "364": [ + "....+.+", + "....+.+", + "+++.+.." + ], + "365": [ + "....+.+", + "....+.+", + "+++.+.+" + ], + "366": [ + "....+.+", + "....+.+", + "+++.++." + ], + "367": [ + "....+.+", + "....+.+", + "+++.+++" + ], + "368": [ + "....+.+", + "....++.", + "+++...." + ], + "369": [ + "....+.+", + "....++.", + "+++...+" + ], + "370": [ + "....+.+", + "....++.", + "+++..+." + ], + "371": [ + "....+.+", + "....++.", + "+++..++" + ], + "372": [ + "....+.+", + "....++.", + "+++.+.." + ], + "373": [ + "....+.+", + "....++.", + "+++.+.+" + ], + "374": [ + "....+.+", + "....++.", + "+++.++." + ], + "375": [ + "....+.+", + "....++.", + "+++.+++" + ], + "376": [ + "....+.+", + "....+++", + "+++...." + ], + "377": [ + "....+.+", + "....+++", + "+++...+" + ], + "378": [ + "....+.+", + "....+++", + "+++..+." + ], + "379": [ + "....+.+", + "....+++", + "+++..++" + ], + "380": [ + "....+.+", + "....+++", + "+++.+.." + ], + "381": [ + "....+.+", + "....+++", + "+++.+.+" + ], + "382": [ + "....+.+", + "....+++", + "+++.++." + ], + "383": [ + "....+.+", + "....+++", + "+++.+++" + ], + "384": [ + "....++.", + ".......", + "+++...." + ], + "385": [ + "....++.", + ".......", + "+++...+" + ], + "386": [ + "....++.", + ".......", + "+++..+." + ], + "387": [ + "....++.", + ".......", + "+++..++" + ], + "388": [ + "....++.", + ".......", + "+++.+.." + ], + "389": [ + "....++.", + ".......", + "+++.+.+" + ], + "390": [ + "....++.", + ".......", + "+++.++." + ], + "391": [ + "....++.", + ".......", + "+++.+++" + ], + "392": [ + "....++.", + "......+", + "+++...." + ], + "393": [ + "....++.", + "......+", + "+++...+" + ], + "394": [ + "....++.", + "......+", + "+++..+." + ], + "395": [ + "....++.", + "......+", + "+++..++" + ], + "396": [ + "....++.", + "......+", + "+++.+.." + ], + "397": [ + "....++.", + "......+", + "+++.+.+" + ], + "398": [ + "....++.", + "......+", + "+++.++." + ], + "399": [ + "....++.", + "......+", + "+++.+++" + ], + "400": [ + "....++.", + ".....+.", + "+++...." + ], + "401": [ + "....++.", + ".....+.", + "+++...+" + ], + "402": [ + "....++.", + ".....+.", + "+++..+." + ], + "403": [ + "....++.", + ".....+.", + "+++..++" + ], + "404": [ + "....++.", + ".....+.", + "+++.+.." + ], + "405": [ + "....++.", + ".....+.", + "+++.+.+" + ], + "406": [ + "....++.", + ".....+.", + "+++.++." + ], + "407": [ + "....++.", + ".....+.", + "+++.+++" + ], + "408": [ + "....++.", + ".....++", + "+++...." + ], + "409": [ + "....++.", + ".....++", + "+++...+" + ], + "410": [ + "....++.", + ".....++", + "+++..+." + ], + "411": [ + "....++.", + ".....++", + "+++..++" + ], + "412": [ + "....++.", + ".....++", + "+++.+.." + ], + "413": [ + "....++.", + ".....++", + "+++.+.+" + ], + "414": [ + "....++.", + ".....++", + "+++.++." + ], + "415": [ + "....++.", + ".....++", + "+++.+++" + ], + "416": [ + "....++.", + "....+..", + "+++...." + ], + "417": [ + "....++.", + "....+..", + "+++...+" + ], + "418": [ + "....++.", + "....+..", + "+++..+." + ], + "419": [ + "....++.", + "....+..", + "+++..++" + ], + "420": [ + "....++.", + "....+..", + "+++.+.." + ], + "421": [ + "....++.", + "....+..", + "+++.+.+" + ], + "422": [ + "....++.", + "....+..", + "+++.++." + ], + "423": [ + "....++.", + "....+..", + "+++.+++" + ], + "424": [ + "....++.", + "....+.+", + "+++...." + ], + "425": [ + "....++.", + "....+.+", + "+++...+" + ], + "426": [ + "....++.", + "....+.+", + "+++..+." + ], + "427": [ + "....++.", + "....+.+", + "+++..++" + ], + "428": [ + "....++.", + "....+.+", + "+++.+.." + ], + "429": [ + "....++.", + "....+.+", + "+++.+.+" + ], + "430": [ + "....++.", + "....+.+", + "+++.++." + ], + "431": [ + "....++.", + "....+.+", + "+++.+++" + ], + "432": [ + "....++.", + "....++.", + "+++...." + ], + "433": [ + "....++.", + "....++.", + "+++...+" + ], + "434": [ + "....++.", + "....++.", + "+++..+." + ], + "435": [ + "....++.", + "....++.", + "+++..++" + ], + "436": [ + "....++.", + "....++.", + "+++.+.." + ], + "437": [ + "....++.", + "....++.", + "+++.+.+" + ], + "438": [ + "....++.", + "....++.", + "+++.++." + ], + "439": [ + "....++.", + "....++.", + "+++.+++" + ], + "440": [ + "....++.", + "....+++", + "+++...." + ], + "441": [ + "....++.", + "....+++", + "+++...+" + ], + "442": [ + "....++.", + "....+++", + "+++..+." + ], + "443": [ + "....++.", + "....+++", + "+++..++" + ], + "444": [ + "....++.", + "....+++", + "+++.+.." + ], + "445": [ + "....++.", + "....+++", + "+++.+.+" + ], + "446": [ + "....++.", + "....+++", + "+++.++." + ], + "447": [ + "....++.", + "....+++", + "+++.+++" + ], + "448": [ + "....+++", + ".......", + "+++...." + ], + "449": [ + "....+++", + ".......", + "+++...+" + ], + "450": [ + "....+++", + ".......", + "+++..+." + ], + "451": [ + "....+++", + ".......", + "+++..++" + ], + "452": [ + "....+++", + ".......", + "+++.+.." + ], + "453": [ + "....+++", + ".......", + "+++.+.+" + ], + "454": [ + "....+++", + ".......", + "+++.++." + ], + "455": [ + "....+++", + ".......", + "+++.+++" + ], + "456": [ + "....+++", + "......+", + "+++...." + ], + "457": [ + "....+++", + "......+", + "+++...+" + ], + "458": [ + "....+++", + "......+", + "+++..+." + ], + "459": [ + "....+++", + "......+", + "+++..++" + ], + "460": [ + "....+++", + "......+", + "+++.+.." + ], + "461": [ + "....+++", + "......+", + "+++.+.+" + ], + "462": [ + "....+++", + "......+", + "+++.++." + ], + "463": [ + "....+++", + "......+", + "+++.+++" + ], + "464": [ + "....+++", + ".....+.", + "+++...." + ], + "465": [ + "....+++", + ".....+.", + "+++...+" + ], + "466": [ + "....+++", + ".....+.", + "+++..+." + ], + "467": [ + "....+++", + ".....+.", + "+++..++" + ], + "468": [ + "....+++", + ".....+.", + "+++.+.." + ], + "469": [ + "....+++", + ".....+.", + "+++.+.+" + ], + "470": [ + "....+++", + ".....+.", + "+++.++." + ], + "471": [ + "....+++", + ".....+.", + "+++.+++" + ], + "472": [ + "....+++", + ".....++", + "+++...." + ], + "473": [ + "....+++", + ".....++", + "+++...+" + ], + "474": [ + "....+++", + ".....++", + "+++..+." + ], + "475": [ + "....+++", + ".....++", + "+++..++" + ], + "476": [ + "....+++", + ".....++", + "+++.+.." + ], + "477": [ + "....+++", + ".....++", + "+++.+.+" + ], + "478": [ + "....+++", + ".....++", + "+++.++." + ], + "479": [ + "....+++", + ".....++", + "+++.+++" + ], + "480": [ + "....+++", + "....+..", + "+++...." + ], + "481": [ + "....+++", + "....+..", + "+++...+" + ], + "482": [ + "....+++", + "....+..", + "+++..+." + ], + "483": [ + "....+++", + "....+..", + "+++..++" + ], + "484": [ + "....+++", + "....+..", + "+++.+.." + ], + "485": [ + "....+++", + "....+..", + "+++.+.+" + ], + "486": [ + "....+++", + "....+..", + "+++.++." + ], + "487": [ + "....+++", + "....+..", + "+++.+++" + ], + "488": [ + "....+++", + "....+.+", + "+++...." + ], + "489": [ + "....+++", + "....+.+", + "+++...+" + ], + "490": [ + "....+++", + "....+.+", + "+++..+." + ], + "491": [ + "....+++", + "....+.+", + "+++..++" + ], + "492": [ + "....+++", + "....+.+", + "+++.+.." + ], + "493": [ + "....+++", + "....+.+", + "+++.+.+" + ], + "494": [ + "....+++", + "....+.+", + "+++.++." + ], + "495": [ + "....+++", + "....+.+", + "+++.+++" + ], + "496": [ + "....+++", + "....++.", + "+++...." + ], + "497": [ + "....+++", + "....++.", + "+++...+" + ], + "498": [ + "....+++", + "....++.", + "+++..+." + ], + "499": [ + "....+++", + "....++.", + "+++..++" + ], + "500": [ + "....+++", + "....++.", + "+++.+.." + ], + "501": [ + "....+++", + "....++.", + "+++.+.+" + ], + "502": [ + "....+++", + "....++.", + "+++.++." + ], + "503": [ + "....+++", + "....++.", + "+++.+++" + ], + "504": [ + "....+++", + "....+++", + "+++...." + ], + "505": [ + "....+++", + "....+++", + "+++...+" + ], + "506": [ + "....+++", + "....+++", + "+++..+." + ], + "507": [ + "....+++", + "....+++", + "+++..++" + ], + "508": [ + "....+++", + "....+++", + "+++.+.." + ], + "509": [ + "....+++", + "....+++", + "+++.+.+" + ], + "510": [ + "....+++", + "....+++", + "+++.++." + ], + "511": [ + "....+++", + "....+++", + "+++.+++" + ] + } +} diff --git a/tests/glyph-examples/conversation.txt b/tests/glyph-examples/conversation.txt new file mode 100644 index 0000000..73871ea --- /dev/null +++ b/tests/glyph-examples/conversation.txt @@ -0,0 +1,8 @@ +# They speak: three statements. A blank line starts the next edge. ++: BODY.OTHER | ONE | BODY.AIR +×: _ | _ | _.WATER +*: _ | _ | _.AIR + ++: SELF | LIGHT.BEFORE | BODY.OTHER + ++: SELF | VOICE.ONE | ONE diff --git a/tests/glyph-examples/how-many.txt b/tests/glyph-examples/how-many.txt new file mode 100644 index 0000000..6f8e67b --- /dev/null +++ b/tests/glyph-examples/how-many.txt @@ -0,0 +1,4 @@ +# "How many pulsars?" Twelve. +# OPEN.COUNT asks "what number?"; the answer COUNT.12 replaces OPEN with COUNT, and COUNT with the bits. ++: STAR.TIME | ONE | OPEN.COUNT +×: _ | _ | COUNT.12 diff --git a/tests/glyph-examples/pulsars.txt b/tests/glyph-examples/pulsars.txt new file mode 100644 index 0000000..32e7099 --- /dev/null +++ b/tests/glyph-examples/pulsars.txt @@ -0,0 +1,5 @@ +# "We see pulsars. The pulsars are twelve." +# The same word on two lines is one node with two edges. ++: SELF | LIGHT.ONE | STAR.TIME + ++: STAR.TIME | ONE | COUNT.12 diff --git a/tests/glyph-examples/question.txt b/tests/glyph-examples/question.txt new file mode 100644 index 0000000..8308015 --- /dev/null +++ b/tests/glyph-examples/question.txt @@ -0,0 +1,3 @@ +# "Your world is what?" and our answer: a water-world. ++: BODY.OTHER | ONE | OPEN +×: _ | _ | BODY.WATER diff --git a/tests/glyph-examples/saw-that.txt b/tests/glyph-examples/saw-that.txt new file mode 100644 index 0000000..3d22412 --- /dev/null +++ b/tests/glyph-examples/saw-that.txt @@ -0,0 +1,5 @@ +# "Your world is water. We saw that." +# A lone ONE in a node slot is "that": the statement on the line above. ++: BODY.OTHER | ONE | BODY.WATER + ++: SELF | LIGHT.BEFORE | ONE diff --git a/tests/glyph-examples/saw-your-world.txt b/tests/glyph-examples/saw-your-world.txt new file mode 100644 index 0000000..77354b2 --- /dev/null +++ b/tests/glyph-examples/saw-your-world.txt @@ -0,0 +1,2 @@ +# "We saw your world." ++: SELF | LIGHT.BEFORE | BODY.OTHER diff --git a/tests/glyph-examples/shadow.txt b/tests/glyph-examples/shadow.txt new file mode 100644 index 0000000..0ddde84 --- /dev/null +++ b/tests/glyph-examples/shadow.txt @@ -0,0 +1,5 @@ +# "We no longer see you." (the shadow transmission) +# Change over time is two edges: we saw you; we do not see you. ++: SELF | LIGHT.BEFORE | OTHER + ++: SELF | LIGHT.NOT | OTHER diff --git a/tests/glyph-examples/third-voice.txt b/tests/glyph-examples/third-voice.txt new file mode 100644 index 0000000..5657576 --- /dev/null +++ b/tests/glyph-examples/third-voice.txt @@ -0,0 +1,4 @@ +# A third voice sides with them, against us, about our own world. ++: BODY.OTHER | ONE | BODY.AIR +×: _ | _ | _.WATER +*: _ | _ | _.AIR diff --git a/tests/glyph.test.mjs b/tests/glyph.test.mjs new file mode 100644 index 0000000..c424a2d --- /dev/null +++ b/tests/glyph.test.mjs @@ -0,0 +1,134 @@ +// lib/glyph.js against glyph-cli: run with `node --test tests/`. +// The fixtures come from the CLI itself (scripts/glyph_fixtures.py); the other cases are ported from its own tests. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { + Vocabulary, GlyphError, parsePage, formatPage, render, renderSvg, decode, readGraph, formatGraph, drawWords, + numberShape, wordStr, word, bandRows, WIDTH, responseText, +} from '../lib/glyph.js'; + +const read = (p) => readFileSync(new URL(p, import.meta.url), 'utf8'); +const DATA = JSON.parse(read('../lib/glyph-vocab.json')); +const ZH = JSON.parse(read('../lib/glyph-zh.json')); +const CLI = JSON.parse(read('./glyph-cli-fixtures.json')); +const vocab = new Vocabulary(DATA); +const page = (text) => parsePage(text, vocab); + +test('the bundled vocabulary is sound', () => { + assert.equal(Object.keys(vocab.parts).length, 16); + assert.deepEqual(vocab.validate(), []); +}); + +for (const [name, ex] of Object.entries(CLI.examples)) { + test(`example ${name} matches glyph-cli`, () => { + const p = page(ex.source); + assert.equal(formatPage(p), ex.formatted); + assert.deepEqual(render(vocab, p), ex.rows); + assert.deepEqual(formatGraph(vocab, readGraph(vocab, p)), ex.graph); + assert.equal(renderSvg(vocab, p), ex.svg); + assert.equal(formatPage(decode(vocab, ex.rows.join('\n'))), ex.decoded); + assert.deepEqual(decode(vocab, ex.rows.join('\n')), p); + }); +} + +test('every vocabulary word draws and reads as in glyph-cli', () => { + assert.equal(vocab.entries.length, Object.keys(CLI.words).length); + for (const e of vocab.entries) { + const want = CLI.words[wordStr(e.word)]; + assert.deepEqual(drawWords(vocab, [e.word]), want.rows, wordStr(e.word)); + assert.equal(vocab.gloss(e.word), want.gloss); + } +}); + +test('parsing words matches glyph-cli', () => { + for (const [text, want] of Object.entries(CLI.parse)) { + if (want.error) { + assert.throws(() => vocab.parse(text), (err) => err instanceof GlyphError && want.error.startsWith(err.message), text); + } else { + const w = vocab.parse(text); + assert.equal(wordStr(w), want.word, text); + assert.equal(vocab.gloss(w), want.node, text); + assert.equal(vocab.gloss(w, 'relation'), want.relation, text); + } + } +}); + +test('numbers are nine bits, as in glyph-cli', () => { + assert.deepEqual(numberShape(137), ['.#.', '..#', '..#']); + assert.deepEqual(numberShape(12), ['...', '..#', '#..']); + for (const [n, rows] of Object.entries(CLI.numbers)) assert.deepEqual(drawWords(vocab, [vocab.parse(`COUNT.${n}`)]), rows); +}); + +test('no number draws like a word', () => { + const words = new Set(vocab.entries.map((e) => bandRows(vocab, [e.word.kind, e.word.which], '#').join('/'))); + for (let n = 1; n < 512; n++) assert.ok(!words.has(bandRows(vocab, ['COUNT', n], '#').join('/')), String(n)); +}); + +test('lines are padded to the page voices', () => { + const rows = render(vocab, page('+: SELF | ONE | OTHER\n×: _ | _ | _.NOT\n\n+: SELF | ONE | SELF')); + assert.equal(rows.length, 7 + 3 + 7); + assert.ok(rows.every((r) => [...r].length === WIDTH)); +}); + +test('numbers read back as numbers', () => { + for (const n of [0, 1, 12, 16, 137, 170, 273, 487, 495, 511]) { + const back = decode(vocab, render(vocab, page(`+: STAR.TIME | ONE | COUNT.${n}`)).join('\n')); + assert.equal(wordStr(back.lines[0].bands[0].words[2]), n === 0 ? 'COUNT' : `COUNT.${n}`); + } + const p = page('+: STAR.TIME | ONE | COUNT.12\n×: _ | _ | _.495'); + assert.deepEqual(decode(vocab, render(vocab, p).join('\n')), p); + const q = page('+: ONE.SELF | ONE | ONE.OTHER'); + assert.deepEqual(decode(vocab, render(vocab, q).join('\n')), q); +}); + +test('pictures are refused', () => { + const dots = '.'.repeat(23) + '\n'; + assert.throws(() => decode(vocab, '+++\n...\n'), /wide/); + assert.throws(() => decode(vocab, '...+...................\n' + dots + dots), /between lattice/); + assert.throws(() => decode(vocab, '+++.×××................\n+.+.×.×................\n+++.×××................\n'), /mixes symbols/); + assert.throws(() => decode(vocab, '+.+....................\n' + dots + dots), /not a part/); + assert.equal(decode(vocab, '').lines.length, 0); +}); + +test('parse errors name the line', () => { + assert.throws(() => page('+: SELF | ONE | OTHER\n+ SELF | ONE | OTHER'), /line 2/); + assert.throws(() => page('+: SELF | ONE'), /three slots/); + assert.throws(() => page('+: SELF | ONE | WIND'), /unknown part/); + assert.throws(() => page('.: SELF | ONE | OTHER'), /symbol/); +}); + +test('rule five', () => { + const cases = [ + ['×: _ | _ | BODY.AIR', ['object.kind: yes (to +)', 'object.which: yes (to +)']], + ['×: _ | _ | _.NOT', ['object.which: no (to +)']], + ['×: _ | _ | _.WATER', ['object.which: instead "water" (to +)']], + ['×: _ | ONE.PATH | _', ['relation.kind: yes (to +)', 'relation.which: and also "way, between"']], + ['×: _ | _ | _', []], + ]; + for (const [reply, want] of cases) { + const g = readGraph(vocab, page('+: BODY.OTHER | ONE | BODY.AIR\n' + reply)); + assert.deepEqual(g.edges[0].replies[0].responses.map((r) => responseText(r)), want); + } + const g = readGraph(vocab, page('+: SELF | LIGHT.BEFORE | ONE')); + assert.equal(formatGraph(vocab, g)[1], '1. (+) we --see [past]--> [that: nothing above]'); +}); + +test('the Chinese glosses cover the whole vocabulary', () => { + for (const name of Object.keys(vocab.parts)) assert.ok(ZH.parts[name]?.thing, name); + for (const m of Object.keys(vocab.markers)) assert.ok(ZH.markers[m], m); + for (const d of vocab.domains()) assert.ok(ZH.domains[d], d); + const seen = new Set(); + for (const e of vocab.entries) { + const z = ZH.words[wordStr(e.word)]; + assert.ok(z?.gloss, wordStr(e.word)); + assert.ok(!seen.has(z.gloss), `duplicate Chinese gloss ${z.gloss}`); + seen.add(z.gloss); + } + assert.deepEqual(Object.keys(ZH.words).sort(), vocab.entries.map((e) => wordStr(e.word)).sort()); +}); + +test('a word built by hand', () => { + assert.deepEqual(vocab.parse('body.star'), word('BODY', 'STAR')); + assert.equal(vocab.gloss(word('BODY', 'STAR')), 'world, matter | star (not in vocabulary)'); +}); From b7906459e1a003ce4edec351bc3b34a23b2bd844 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 21:34:47 +0000 Subject: [PATCH 2/3] Netlify: publish the static site as-is (same change as #9) Deploy previews still use the old Hugo build settings and fail on plain static files. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01RH7A8tqLXxxGiks9mUWtyE --- netlify.toml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 netlify.toml diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..ae08e7f --- /dev/null +++ b/netlify.toml @@ -0,0 +1,6 @@ +# The live site is served by GitHub Pages from main. Netlify still builds +# deploy previews for pull requests with the old Hugo settings, which fail +# now that the site is plain static files. Publish the repo root as-is. +[build] + publish = "." + command = "echo 'static site, no build step'" From e4de93d2d64dbe365028d3732300902f1d8e4f2a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 21:35:25 +0000 Subject: [PATCH 3/3] Drop the netlify.toml port: the previews still fail with it, so it is not the fix Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01RH7A8tqLXxxGiks9mUWtyE --- netlify.toml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 netlify.toml diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index ae08e7f..0000000 --- a/netlify.toml +++ /dev/null @@ -1,6 +0,0 @@ -# The live site is served by GitHub Pages from main. Netlify still builds -# deploy previews for pull requests with the old Hugo settings, which fail -# now that the site is plain static files. Publish the repo root as-is. -[build] - publish = "." - command = "echo 'static site, no build step'"