From f00862e388946aac1e8d0ac88c195d44ebc0ee77 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 22:51:27 +0000 Subject: [PATCH 1/5] Signals: add the resonance theory and read each transmission through it Adds a 'Speech that needs two' section to /signals/ (CN and EN) and a short resonance-theory reading under each of the five transmissions. Framed as a working theory held by listeners in the story, not a decoding. Encodings and audio are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QgskKyin9t8k9F6KcUj5o7 --- app/signals.css | 4 ++++ signals/en/index.html | 17 ++++++++++++++++- signals/index.html | 17 ++++++++++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/app/signals.css b/app/signals.css index 59c1d4a..f3ffced 100644 --- a/app/signals.css +++ b/app/signals.css @@ -15,6 +15,10 @@ .signals-split .scope canvas { height: clamp(16rem, 34vw, 24rem); } #cv-picture { image-rendering: pixelated; } +/* Readings under the resonance theory: a working theory in the story, not a decoding. */ +.signals-split .theory { padding-left: 0.9rem; border-left: 4px solid var(--them); } +.signals-split .theory strong { color: var(--them); } + .tick-us, .tick-them { font-style: normal; } .tick-us { color: var(--us); } .tick-them { color: var(--them); } diff --git a/signals/en/index.html b/signals/en/index.html index 4719d59..b0e9030 100644 --- a/signals/en/index.html +++ b/signals/en/index.html @@ -40,6 +40,16 @@

One beacon, five transmissions.

+
+
+

A working theory

+

Speech that needs two

+

Nobody on Earth knows what they look like. But the transmissions have led many listeners to one theory: they speak by resonance, in air. Alone, a body can only ring at its own tones, coloured by the place it stands. That is enough to say "I am here", and little more. When a second voice is near (a person, a machine, even the wind), new tones appear that neither could make alone. Tones split, sum and difference tones sound, and the two either fall into step or beat against each other. In this theory, that is where language lives.

+

If it is right, the beacon is only half of a conversation. It holds its note, with gaps where a partner's tones would go. To understand it fully, someone has to answer.

+

Below, each transmission is read in two ways: what it encodes, and what it might mean if the theory is right.

+
+
+
@@ -47,6 +57,7 @@

One beacon, five transmissions.

667 bits, folded once

Clicks spaced 2, 3, 5, 7 … 47 apart say "this is deliberate". Then come 667 bits, high tone for 1 and low tone for 0. 667 = 23 × 29, so there is only one way to lay them out as a picture: counting, their star and its one planet, a dish, and the hydrogen spin flip.

This is the naive one: it assumes we see the way they do. Fold it yourself.

+

Resonance theory. A picture is the most foreign thing a resonant species could make, and they made it because we see. Its two tones may stand for the two states of a pair: in step, and beating.

23 × 29 · 1 = high tone, 0 = low tone–
@@ -60,6 +71,7 @@

667 bits, folded once

Their first word is "you"

The tone dims and recovers, again and again. Each dip is Earth crossing the Sun, exactly as it looked from Ross 128: 84 parts per million deep, rounded by the Sun's darker edge. They send one dip per century of the years they watched, from 1100 BCE to 1100 CE. The dips start short and grazing, grow to 13 hours, then shrink again, because Ross 128 was drifting across the band from which Earth's transits can be seen. After that comes silence: two centuries with no dip. Then three short, deep dips: their own planet crossing their star, every 9.87 days.

Every dip comes from the real motion of the two stars (Drift). The pattern says "we watched you for 2,200 years, and then we lost you".

+

Resonance theory. Earth crossing the Sun once a year is a natural rhythm, our planet's own tone. They name us by our rhythm, and themselves by theirs.

brightness · dips ×7,000 so you can see them–
@@ -73,6 +85,7 @@

Their first word is "you"

An address written in clocks

Twelve pulse trains, one after another, each at a real pulsar's period. A millisecond pulsar spins fast enough to be a note: B1937+21 hums at 642 Hz. A slow one ticks. The periods name the pulsars, so there is no need to draw where they are; any astronomer already knows.

What matters is when each train starts. They begin it at the phase of the pulsar as it reaches Ross 128. A pulse reaches them earlier or later than it reaches us, depending on how far they sit along that pulsar's direction. Twelve phase shifts with only three unknowns pin down where they are and, read against the pulsar clocks, when they sent it. This is how the MC ledger keeps time, so the ledger can read it directly.

+

Resonance theory. The pulsars are not only the address but the partner. Heard together with the real pulsars, the trains would make tones that neither makes alone. That may be why the signal turned up in pulsar-timing data at all: it was never meant to be heard by itself.

one period per row · ▮ us · ▮ them–
@@ -86,6 +99,7 @@

An address written in clocks

Words made of molecules

Short bursts at several frequencies at once. Each frequency is a real radio line, the note a molecule rings at: hydrogen at 1420 MHz, water at 22 GHz, oxygen at 119 GHz. A chord names a substance and its loudness says how much. Every line is shifted up by the same 0.01%, the same Doppler shift as the hydrogen tone, so they all come from the same place.

Five chords so far: hydrogen as the unit; water; much water; their air, with oxygen and ozone; and methanol beside three lines no catalogue lists.

+

Resonance theory. Molecules are resonators too, and air is what carries their speech. The chord of their air may be its tuning, and the three unknown lines may be gases they added to set it.

frequency, 1–130 GHz (log) · bar height = loudness–
@@ -99,6 +113,7 @@

Words made of molecules

137, with no digits

A fast buzz of clicks and a slow, bright pip. Count the clicks between pips: 137. But the pip doesn't land on a click. Each round it slips a little further, 0.036 of a click, and after about 28 rounds it lines up again. The ratio of the two rhythms is 137.036.

That is the inverse of the fine-structure constant, the strength of light's grip on electrons. It has no units, so it is the same number in any system of measurement on any world. Sending it as a ratio says three things at once: we know physics, our physics is yours, and here is how precisely we measured it.

+

Resonance theory. Two rhythms that never quite agree, with the meaning in the slip between them. If they speak by resonance, this is the simplest complete sentence they could send, and the natural first lesson.

one row per round · zoomed in on the gap between two clicks–
@@ -115,7 +130,7 @@

How the sound is made

  • For the chords, the line frequencies are real (CDMS/JPL). Every factor of four in radio frequency becomes one octave, with 1420 MHz on the G below middle C. The three unknown lines are invented.
  • For the number, 1/α = 137.035999084 (CODATA 2018).
  • -

    The rotation of the five transmissions, their content and their order are story proposals.

    +

    The rotation of the five transmissions, their content and their order are story proposals. The resonance readings are a theory held by listeners in the story, not a decoding.

    diff --git a/signals/index.html b/signals/index.html index e939bbc..84bcd41 100644 --- a/signals/index.html +++ b/signals/index.html @@ -40,6 +40,16 @@

    一个信标,五段发射。

    +
    +
    +

    一个推测

    +

    需要两个声音的语言

    +

    地球上没有人知道他们长什么样。但这几段发射让许多收听者想到同一个推测:他们靠共振说话,借助空气。一个身体独自只能以自己的音调振动,再带上所处地方的音色。这足以说“我在这里”,也仅此而已。当另一个声音在旁边,无论是另一个人、一台机器,还是风,就会出现两者单独都发不出的音:音调分裂,和音与差音响起,两者要么合拍,要么互相拍频。按这个推测,语言就在这里。

    +

    如果推测是对的,信标只是一场对话的一半。它持续发着自己的音,在伙伴的音该在的地方留出空隙。要完全听懂它,就得有人回答。

    +

    下面每一段都有两种读法:它编码了什么,以及如果推测成立,它可能是什么意思。

    +
    +
    +
    @@ -47,6 +57,7 @@

    一个信标,五段发射。

    667 位,折一次

    间隔 2、3、5、7……47 的咔嗒声在说“这是故意的”。接着是 667 位,1 是高音,0 是低音。667 = 23 × 29,所以只有一种排法能成图:计数、他们的星和唯一的行星、一面天线、氢的自旋翻转。

    这是最朴素的一段:它假设我们和他们看东西的方式一样。自己折一次。

    +

    共振推测: 对一个靠共振说话的物种来说,图是最陌生的东西。他们画了,是因为我们靠眼睛。两种音调也许代表一对声音的两种状态:合拍,和拍频。

    23 × 29 · 1 = 高音,0 = 低音–
    @@ -60,6 +71,7 @@

    667 位,折一次

    他们的第一个词是“你们”

    音调一次次变暗又恢复。每一次变暗,都是地球从太阳前经过,正是从罗斯128看到的样子:深百万分之八十四,被太阳较暗的边缘磨圆。他们观察我们的那些年里,每个世纪发一次,从公元前约 1100 年到公元 1100 年。起初很短、擦边而过,后来长到 13 小时,再慢慢变短,因为罗斯128正在漂过那条能看到地球凌日的带子。然后是两个世纪的静默。最后是三次又短又深的变暗:他们自己的行星每 9.87 天从他们的恒星前经过一次。

    每一次变暗都来自两颗星的真实运动(两星漂移)。整段意思是:“我们看了你们两千二百年,然后看不见了。”

    +

    共振推测: 地球每年从太阳前经过一次,是一种天然的节奏,是我们星球自己的音。他们用我们的节奏称呼我们,用他们的节奏称呼自己。

    亮度 · 变暗放大 7,000 倍–
    @@ -73,6 +85,7 @@

    他们的第一个词是“你们”

    用时钟写成的地址

    十二串脉冲,一串接一串,每一串都按一颗真实脉冲星的周期。毫秒脉冲星转得够快,听起来是一个音:B1937+21 以 642 Hz 嗡鸣。慢的只是滴答声。周期就是名字,所以不必画出它们在哪里,天文学家都知道。

    关键是每一串从哪里开始。他们按脉冲星传到罗斯128时的相位起头。同一个脉冲到达他们那里比到达我们早一些或晚一些,取决于他们在这颗脉冲星的方向上离我们多远。十二个相位差,只有三个未知数,足以定出他们在哪里;再对照脉冲星时钟,还能定出发送的时刻。脉币账本正是这样计时的,所以账本可以直接读它。

    +

    共振推测: 脉冲星不只是地址,也是伙伴。这些脉冲串和真实的脉冲星一起听,会产生两者单独都没有的音。这也许正是信号会出现在脉冲星计时数据里的原因:它本来就不是给人单独听的。

    每行一个周期 · ▮ 我们 · ▮ 他们–
    @@ -86,6 +99,7 @@

    用时钟写成的地址

    用分子组成的词

    几个频率同时响起的短促爆发。每个频率都是一条真实的射电谱线,是某种分子特有的音:氢在 1420 MHz,水在 22 GHz,氧在 119 GHz。一个和弦说出一种物质,响度说明有多少。每条谱线都同样偏高 0.01%,和氢音调的多普勒频移一样,说明它们来自同一个地方。

    目前有五个和弦:氢,作为单位;水;很多水;他们的空气,有氧和臭氧;还有甲醇,旁边是三条任何目录里都没有的谱线。

    +

    共振推测: 分子也是共振体,而空气承载着他们的语言。他们空气的和弦也许是一种调音,那三条未知谱线也许是他们为了定调而加入的气体。

    频率 1–130 GHz(对数)· 柱高 = 响度–
    @@ -99,6 +113,7 @@

    用分子组成的词

    137,不用数字

    一串急促的咔嗒声,和一个缓慢、明亮的提示音。数一数两次提示音之间的咔嗒声:137。但提示音并不正好落在咔嗒声上。每一轮它都往后滑一点,是一个间隔的 0.036,大约 28 轮之后又对齐。两种节奏之比是 137.036。

    这是精细结构常数的倒数,代表光与电子相互作用的强弱。它没有单位,所以在任何世界、任何度量制里都是同一个数。用比例发送它,同时说了三件事:我们懂物理;我们的物理就是你们的物理;这是我们测量它的精度。

    +

    共振推测: 两种节奏永远对不齐,意义就在两者的滑移里。如果他们靠共振说话,这是他们能发出的最简单的完整句子,也是最自然的第一课。

    每行一轮 · 放大到两下咔嗒声之间–
    @@ -115,7 +130,7 @@

    声音是怎么做的

  • 和弦:谱线频率是真实的(CDMS/JPL)。射电频率每乘 4 升一个八度,1420 MHz 落在中央 C 下面的 G 上。三条未知谱线是虚构的。
  • 数字:1/α = 137.035999084(CODATA 2018)。
  • -

    五段发射的轮换方式、内容和顺序都是故事提案。

    +

    五段发射的轮换方式、内容和顺序都是故事提案。共振的读法是故事中收听者的推测,不是破译结果。

    From 7f63d7ad414f3e86c2191d7ec2fb2a2db942e98c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 08:37:23 +0000 Subject: [PATCH 2/5] Netlify: publish the static site as-is so deploy previews stop failing The old Netlify site still builds previews with its Hugo settings, which fail since the Hugo config was removed in #3. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QgskKyin9t8k9F6KcUj5o7 --- 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 c376fcf72289b81b4a4649c6fca5366b756f1969 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 04:30:02 +0000 Subject: [PATCH 3/5] Remove netlify.toml: the site is on GitHub Pages and the Netlify setup is being retired 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'" From 15a263e828e43791c04bcbbec77d5d535351185b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 05:27:05 +0000 Subject: [PATCH 4/5] Grid script pages: grammar v8 (triplet spacing, base-512 numbers) lib/glyph.js now ports glyph-cli 0.3.0: words are spaced 1/2/4 cells, triplets run left to right, numbers are COUNT plus base-512 digits, and decode reads the structure from the gaps. The explainer, converter and dictionary pages describe and use the new rules; the converter gets a triplets-per-line choice, a free-width drawing grid and a warning for numbers whose trailing 0 digit would vanish at a line end. Vocabulary and fixtures regenerated from glyph-cli 0.3.0 (InterImm/glyph-cli#5). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QgskKyin9t8k9F6KcUj5o7 --- app/script.js | 104 +++--- lib/glyph-vocab.json | 10 +- lib/glyph-zh.json | 2 +- lib/glyph.js | 401 ++++++++++++++++------ script/convert/en/index.html | 11 +- script/convert/index.html | 13 +- script/en/index.html | 26 +- script/index.html | 26 +- scripts/glyph_fixtures.py | 12 +- tests/glyph-cli-fixtures.json | 477 ++++++++++++++++++-------- tests/glyph-examples/conversation.txt | 2 +- tests/glyph-examples/how-many.txt | 2 +- tests/glyph-examples/numbers.txt | 5 + tests/glyph-examples/pulsars.txt | 2 +- tests/glyph-examples/saw-that.txt | 2 +- tests/glyph.test.mjs | 53 ++- 16 files changed, 786 insertions(+), 362 deletions(-) create mode 100644 tests/glyph-examples/numbers.txt diff --git a/app/script.js b/app/script.js index 8021a1b..2a0f439 100644 --- a/app/script.js +++ b/app/script.js @@ -3,7 +3,8 @@ // 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, + pageSymbols, wordStr, isEmpty, structure, ink, numberShape, positions, toDigits, hiddenZeros, + EMPTY_CELL, BACKGROUND, GRID, BASE, } from '../lib/glyph.js'; const zh = document.documentElement.lang.toLowerCase().startsWith('zh'); @@ -21,37 +22,37 @@ const pageUrl = (name, source) => { }; const T = zh ? { - slots: { subject: '前节点', relation: '关系', object: '后节点' }, halves: { kind: '类', which: '指' }, + slots: { subject: '前节点', relation: '关系', object: '后节点' }, halves: { kind: '类', which: '指' }, digit: (n) => `第 ${n} 位`, meaning: { yes: '是', no: '不', instead: '换成', 'and also': '还有' }, to: (s) => `(回应 ${s})`, quote: (s) => `“${s}”`, noComment: '在场,未表态', - statement: (n) => `[第 ${n} 句]`, thatNothing: '[“那”:上面没有句子]', notInVocabulary: '(不在词典中)', + statement: (n) => `[第 ${n} 个三元组]`, thatNothing: '[“那”:前面没有三元组]', notInVocabulary: '(不在词典中)', part: '部件', asThing: '作为事物', asRelation: '作为关系', none: '—', results: (n) => `${n} 个词`, noResults: '没有找到。试试别的词,或者直接写出一个词,比如 BODY.STAR。', - composed: '拼出来的词', inVocab: '词典里的词', notIn: '还不在词典里:这是按部件拼出的读法。', number: '数', numberNote: '数是内置的:COUNT 后面的九个比特。', + composed: '拼出来的词', inVocab: '词典里的词', notIn: '还不在词典里:这是按部件拼出的读法。', number: '数', numberNote: '数是内置的:COUNT 后面跟着 512 进制的数位,每位九个比特。', + hiddenZero: (n) => `第 ${n} 个三元组在行尾以 0 数位结尾。0 位画出来是空的,读的人看不出这个数在哪里结束:把它放到前节点,或者在后面再写一个三元组。`, 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}`, + unknownPart: (a) => `未知的部件:${a.part}`, digit: (a) => `每个数位是 0–511(512 进制):${a.s}`, + leadingZero: (a) => `数不以 0 数位开头:${a.s}`, tooLong: (a) => `只有数才会多于两个位置:${a.s}`, whole: (a) => `只能是零或正整数:${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} 个位置:这里既不是部件,也不是数`, + structure: () => '这些排分不成行和声部;这是一幅图,而不是文字吗?', + gaps: (a) => `第 ${a.from}–${a.to} 排:这些记号按 1、2、4 格的空隙切不成 3 × 3 的部件(是一幅图吗?)`, + mixed: (a) => `第 ${a.from}–${a.to} 排:一个声部里混用了几种符号`, }, } : { - slots: { subject: 'subject', relation: 'relation', object: 'object' }, halves: { kind: 'kind', which: 'which' }, + slots: { subject: 'subject', relation: 'relation', object: 'object' }, halves: { kind: 'kind', which: 'which' }, digit: (n) => `digit ${n}`, 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)', + statement: (n) => `[triplet ${n}]`, thatNothing: '[that: nothing before]', 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.', + 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: COUNT and base-512 digits, nine bits each.', + hiddenZero: (n) => `Triplet ${n} ends its line with a number ending in a 0 digit. A 0 digit is drawn blank, so a reader can't see where the number ends: move it to the subject, or put another triplet after it.`, 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: {}, @@ -84,8 +85,8 @@ async function loadVocab() { // ------------------------------------------------------------------ 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 svgOf = (rows, symbols, cell = 12, width) => rowsSvg(rows, symbols, { cell, width }); +const wordSvg = (vocab, w, symbol = '+') => svgOf(bandRows(vocab, positions(w), symbol), [symbol], 12); const partSvg = (shape, symbol = '+') => svgOf(shape.map((r) => r.replaceAll('#', symbol)), [symbol], 12, 3); function graphHtml(voc, g, symbols) { @@ -96,7 +97,7 @@ function graphHtml(voc, g, symbols) { ${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('')} + `${T.slots[x.slot]}.${T.halves[x.half] ?? T.digit(x.half.split(' ')[1])} ${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('')} `).join('')}`; } @@ -129,14 +130,19 @@ function explainer({ en, voc }) { 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; + n = Math.max(0, Math.min(+num.max || 511, Math.round(+n || 0))); + num.value = n; + input.value = Math.min(n, 511); const w = en.parse(`COUNT.${n}`); out.innerHTML = wordSvg(en, w); - const sh = numberShape(n); + // the bits of the last digit; a bigger number shows how its digits add up + const ds = toDigits(n); + const sh = numberShape(ds[ds.length - 1]); 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); + const term = (d, p) => (p === 0 ? `${d}` : p === 1 ? `${d} × ${BASE}` : `${d} × ${BASE}^${p}`); + const sum = ds.length > 1 ? ` = ${ds.map((d, i) => term(d, ds.length - 1 - i)).join(' + ')}` : ''; + $('.gs-number-word', el).textContent = wordStr(w) + sum; }; input.addEventListener('input', () => show(input.value)); num.addEventListener('input', () => show(num.value)); @@ -179,7 +185,7 @@ function dictionary({ en, voc }) { 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)) { + if (t && /^[a-z_]+(\.[a-z0-9_]*)*$/i.test(t)) { try { const w = en.parse(t); if (!isEmpty(w)) { @@ -213,18 +219,21 @@ 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'); + const examples = $('#gs-examples'), perLineIn = $('#gs-per-line'); let lastRows = []; + const perLine = () => +perLineIn.value || 2; // ---- 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 }) : ''; + lastRows = render(en, page, perLine()); + out.innerHTML = page.triplets.length ? renderSvg(en, page, { cell: 16, perLine: perLine() }) : ''; rowsOut.textContent = lastRows.join('\n'); - graphOut.innerHTML = page.lines.length ? graphHtml(voc, readGraph(voc, page), pageSymbols(page)) : ''; - errOut.hidden = true; + graphOut.innerHTML = page.triplets.length ? graphHtml(voc, readGraph(voc, page), pageSymbols(page)) : ''; + const hidden = hiddenZeros(page, perLine()); + errOut.textContent = hidden.map(T.hiddenZero).join(' '); + errOut.hidden = !hidden.length; src.removeAttribute('aria-invalid'); } catch (err) { errOut.textContent = message(err); errOut.hidden = false; @@ -232,6 +241,7 @@ function converter({ en, voc }) { } }; src.addEventListener('input', fromSource); + perLineIn.addEventListener('change', fromSource); examples.addEventListener('change', () => { const opt = examples.selectedOptions[0]; if (!opt.value) return; @@ -240,7 +250,7 @@ function converter({ en, voc }) { $('#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 blob = new Blob([renderSvg(en, parsePage(src.value, en), { perLine: perLine() })], { 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 */ } @@ -258,7 +268,7 @@ function converter({ en, voc }) { // ---- 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'); + const linesIn = $('#gs-lines'), voicesIn = $('#gs-voices'), palette = $('#gs-palette'), numIn = $('#gs-num'), widthIn = $('#gs-width'); let grid = []; // rows of characters let symbol = '+'; let tool = 'BODY'; // a part name, 'NUMBER', 'CELL' or 'ERASE' @@ -266,21 +276,26 @@ function converter({ en, voc }) { const heightFor = (lines, voices) => lines * (4 * voices - 1) + LINE_GAP_ROWS * (lines - 1); const LINE_GAP_ROWS = 3; + const cols = () => Math.max(7, Math.min(120, Math.round(+widthIn.value || 45))); const resize = (h) => { - const blank = () => Array(WIDTH).fill(EMPTY_CELL); - grid = Array.from({ length: h }, (_, r) => grid[r] || blank()); + const w = cols(); + grid = Array.from({ length: h }, (_, r) => { + const row = (grid[r] || []).slice(0, w); + while (row.length < w) row.push(EMPTY_CELL); + return row; + }); }; 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 + // the rows where bands go are a shade lighter, so the grid shows where parts can sit 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 w = (cols() + 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; + const fill = ch !== EMPTY_CELL ? ink(ch, symbols) : onRow ? '#26339a' : GRID; out.push(``); }); }); @@ -290,8 +305,10 @@ function converter({ en, voc }) { 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]); + if (rows.length) { + const w = Math.max(...rows.map((r) => [...r].length)); + grid = rows.map((r) => { const row = [...r]; while (row.length < w) row.push(EMPTY_CELL); return row; }); + widthIn.value = w; // the voices per line as the machine reads them, so the editor shades the right positions try { const v = structure(rows); @@ -303,7 +320,7 @@ function converter({ en, voc }) { try { const page = decode(en, drawing.value); back.textContent = formatPage(page); - backGraph.innerHTML = page.lines.length ? graphHtml(voc, readGraph(voc, page), pageSymbols(page)) : ''; + backGraph.innerHTML = page.triplets.length ? graphHtml(voc, readGraph(voc, page), pageSymbols(page)) : ''; backErr.hidden = true; } catch (err) { back.textContent = ''; backGraph.innerHTML = ''; @@ -313,12 +330,13 @@ function converter({ en, voc }) { 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); + widthIn.addEventListener('change', () => { widthIn.value = cols(); 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: 'NUMBER', label: zh ? '数位' : 'digit', shape: numberShape(5) }, { id: 'CELL', label: zh ? '单格' : 'one cell', shape: ['...', '.#.', '...'] }, { id: 'ERASE', label: zh ? '擦除' : 'erase', shape: ['...', '...', '...'] }]; palette.innerHTML = tools.map((t) => `

    Text to grid, grid to text

    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.

    +

    One band per row: SYMBOL: node | relation | node. Words are KIND.WHICH from the dictionary, numbers are COUNT and base-512 digits (COUNT.137, COUNT.4.171 for 2219, or just COUNT.2219), and _ is empty. A block of rows is one triplet; rows under its first are other voices answering it. A blank row starts the next triplet. # starts a comment.

    +

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

    @@ -81,7 +83,7 @@

    Text → drawing

    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.

    +

    Pick a part and click to stamp it, or pick "one cell" and draw freely. The lighter rows are where bands go. Leave 1 empty cell between the parts of a word, 2 between words and 4 between triplets: the machine reads the structure from those gaps alone, and anything that won't split into whole parts is a picture. You can also paste a drawing as text, one row per line, with . for empty cells.

    @@ -95,8 +97,9 @@

    Drawing → text

    - + +
    diff --git a/script/convert/index.html b/script/convert/index.html index 1da1428..3124a26 100644 --- a/script/convert/index.html +++ b/script/convert/index.html @@ -41,14 +41,14 @@

    文字变图,图变文字

    文字 → 图

    -

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

    +

    每行一个声部:符号: 节点 | 关系 | 节点。词写成词典里的 类.指,数写成 COUNT 加 512 进制的数位(COUNT.137;2219 写作 COUNT.4.171,也可以直接写 COUNT.2219),_ 表示空。一组行是一个三元组,第一行下面的行是其他声音在回应它。空一行开始下一个三元组。# 后面是注释。

    +

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

    @@ -81,7 +83,7 @@

    文字 → 图

    图 → 文字

    -

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

    +

    选一个部件,点一下把它盖上去;或者选“单格”随手画。颜色浅一些的排是声部所在的地方。词里的部件之间空 1 格,词与词空 2 格,三元组之间空 4 格:翻译机只靠这些空隙读出结构,切不成完整部件的就是一幅图。也可以粘贴用文字表示的图,每行一排,. 表示空格。

    @@ -95,8 +97,9 @@

    图 → 文字

    - - + + +
    diff --git a/script/en/index.html b/script/en/index.html index e842778..f64407d 100644 --- a/script/en/index.html +++ b/script/en/index.html @@ -51,7 +51,8 @@

    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.
  • +
  • Spacing is structure: 1 empty cell between the parts of a word, 2 between the words of a triplet, 4 between triplets.
  • +
  • A triplet 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.

    @@ -75,7 +76,7 @@

    Sixteen parts

    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.

    +

    A word is two parts, one empty cell apart: 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 is just 3 cells wide. The dictionary lists every agreed word.

      @@ -85,8 +86,8 @@

      Two parts make a 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.

      +

      Every triplet is one edge of a graph

      +

      A text is a run of triplets (node, relation, node), read left to right and wrapping onto new lines. Each triplet is one edge of a knowledge graph, so a text is a graph. The gaps say it all: 1 cell inside a word, 2 between words, 4 between triplets, 3 rows between lines. The same word in two triplets is the same node, and a lone ONE in a node slot means "that": the triplet before.

      @@ -118,20 +119,21 @@

      Every line is one edge of a graph

      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.

      +

      Numbers are base-512 digits

      +

      A number is COUNT, a counting line, followed by base-512 digits, most significant first. Each digit is nine bits: 256 down to 1, left to right, top to bottom. COUNT alone is zero; 137 is COUNT.137; the year 2219 is COUNT.4.171, because 4 × 512 + 171 = 2219. Only the positions after COUNT are read as bits, so a number can never be mistaken for a word.

      - + -

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

      +

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

      +
      @@ -141,7 +143,7 @@

      Numbers are nine bits

      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.

      +

      Only a new party adds a band under a triplet, 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 of the same word. That lets anyone agree with, deny or change any node or edge, part by part. A conversation is a graph several voices build together.

      @@ -167,9 +169,9 @@

      Stacking means another voice

      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. Split the page into lines at the three-row gaps, and count the bands in a line: that is the number of voices.
      8. +
      9. Cut each line into triplets, words and parts at the 1, 2 and 4 cell gaps. Anything that won't cut into whole parts is a picture, like the first 667-bit message, not text.
      10. +
      11. Read the first band of each triplet as an edge: node, relation, node. Same word, same node. A lone ONE points at the triplet before.
      12. 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.

      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 index 1ddcd92..f5372d7 100644 --- a/script/index.html +++ b/script/index.html @@ -51,7 +51,8 @@

      一张网格,每个声音一种记
    • 一切都画在同一张共享的网格上。
    • 每一方只用自己的记号画:+ 是罗斯128 b,× 是我们。记号从不互相覆盖,所以总能看出是谁画的。
    • 一块 3 × 3 的格子是一个部件,两个部件组成一个词,就像笔画组成汉字。它不是字母表。
    • -
    • 一行是一句话。其他声音在它下面画出自己的回答。
    • +
    • 空隙就是结构:词里的部件隔 1 格,三元组里的词隔 2 格,三元组之间隔 4 格。
    • +
    • 一个三元组是一句话。其他声音在它下面画出自己的回答。
    • 到目前为止,他们发来的一切都只用 + 画成。图里的第二个声部是人类可以发回去的回答:是水,不是空气。

      @@ -75,7 +76,7 @@

      十六个部件

      3 · 词

      两个部件组成一个词

      -

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

      +

      一个词是两个部件,中间空一格:左边是类,右边是指,写作 类.指。属于你们的世界是你们的世界;计时的恒星是脉冲星;行进的空气是风。只有一个部件的词就只有 3 格宽。所有约定好的词都在词典里。

        @@ -85,8 +86,8 @@

        两个部件组成一个词

        4 · 句

        -

        每一行是图谱的一条边

        -

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

        +

        每个三元组是图谱的一条边

        +

        一段文字是一串三元组(节点,关系,节点),从左往右读,一行写不下就换行。每个三元组是知识图谱的一条边,所以一段文字就是一张图。空隙说明一切:词里隔 1 格,词与词隔 2 格,三元组之间隔 4 格,行与行隔 3 排。同一个词出现在两个三元组里,就是同一个节点;节点位置上单独一个 ONE 表示“那”,指前一个三元组。

        @@ -118,20 +119,21 @@

        每一行是图谱的一条边

        5 · 数

        -

        数是九个比特

        -

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

        +

        数是 512 进制的数位

        +

        一个数写作 COUNT(一条计数的横线),后面跟着 512 进制的数位,高位在前。每个数位按九个比特读:从 256 到 1,从左到右,从上到下。COUNT 单独出现是零;137 是 COUNT.137;2219 年是 COUNT.4.171,因为 4 × 512 + 171 = 2219。只有 COUNT 后面的位置按比特读,所以数永远不会被错认成词。

        - + -

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

        +

        写作 COUNT.137。试试 137(第五段发射),或者 2219。

        +
        @@ -141,7 +143,7 @@

        数是九个比特

        6 · 声音

        往下叠,就是又一个声音

        -

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

        +

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

        BelowMeans
        @@ -167,9 +169,9 @@

        往下叠,就是又一个声音7 · 翻译机

        翻译机怎样读一页

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

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

        diff --git a/scripts/glyph_fixtures.py b/scripts/glyph_fixtures.py index 940b39c..4ec7fa2 100644 --- a/scripts/glyph_fixtures.py +++ b/scripts/glyph_fixtures.py @@ -3,7 +3,7 @@ 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 + pip install glyph-cli==0.3.0 python3 scripts/glyph_fixtures.py """ @@ -25,6 +25,7 @@ page = parse_page(source, vocab) rows = render(vocab, page) examples[path.name] = { + "rows_one_per_line": render(vocab, page, per_line=1), "source": source, "formatted": format_page(page), "rows": rows, @@ -39,14 +40,19 @@ 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"]: + "LIGHT.VOICE", "BODY.WIND", "COUNT.512", "STAR.12", "ONE.12", "COUNT.SELF", "FOO", "_.0", + "COUNT.4.171", "COUNT.2219", "COUNT.70491", "COUNT.12.106.0", "COUNT.4.512", "COUNT.0.5", "_.4.170", + "BODY.SELF.OTHER"]: 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)} +numbers = { + str(n): draw_words(vocab, [vocab.parse(f"COUNT.{n}")]) + for n in [*range(0, 512), 512, 2219, 70491, 262143, 262144, 3200000] +} 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") diff --git a/tests/glyph-cli-fixtures.json b/tests/glyph-cli-fixtures.json index 6a6b156..7401943 100644 --- a/tests/glyph-cli-fixtures.json +++ b/tests/glyph-cli-fixtures.json @@ -1,49 +1,76 @@ { - "glyph_cli": "0.2.0", + "glyph_cli": "0.3.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", + "rows_one_per_line": [ + "+++.+++.......+++.+.+", + "+++.+.....+...+++....", + "+++.+++.......+++.+.+", + ".....................", + "..................×××", + ".....................", + "..................×××", + ".....................", + "..................*.*", + ".....................", + "..................*.*", + ".....................", + ".....................", + ".....................", + "+++..+.+.+....+++.+++", + "+.+...+..+++..+++.+..", + "+++..+.+.+....+++.+++", + ".....................", + ".....................", + ".....................", + ".....................", + ".....................", + ".....................", + ".....................", + ".....................", + ".....................", + ".....................", + ".....................", + "+++..+...............", + "+.+..++...+....+.....", + "+++..+++.............", + ".....................", + ".....................", + ".....................", + ".....................", + ".....................", + ".....................", + ".....................", + "....................." + ], + "source": "# They speak: three triplets. A blank line starts the next one.\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:", @@ -51,27 +78,36 @@ " (×) object.which: instead \"water\" (to +)", " (*) object.which: instead \"air\" (to ×)", "2. (+) we --see [past]--> your world", - "3. (+) we --say, send [now]--> [statement 2]", + "3. (+) we --say, send [now]--> [triplet 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", + "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\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\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", + "rows_one_per_line": [ + ".+..+.+........+.....", + "+++.+.+...+...+.+....", + ".+..+.+........+..+++", + ".....................", + ".....................", + "....................×", + "..............×××.×.." + ], + "source": "# \"How many pulsars?\" Twelve.\n# OPEN.COUNT asks \"what number?\"; the answer COUNT.12 replaces OPEN with COUNT, and COUNT with the digit.\n+: STAR.TIME | ONE | OPEN.COUNT\n×: _ | _ | COUNT.12\n", "formatted": "+: STAR.TIME | ONE | OPEN.COUNT\n×: _ | _ | COUNT.12\n", "rows": [ - ".+..+.+..........+.....", - "+++.+.+..+......+.+....", - ".+..+.+..........+..+++", - ".......................", - ".......................", - "......................×", - "................×××.×.." + ".+..+.+........+.....", + "+++.+.+...+...+.+....", + ".+..+.+........+..+++", + ".....................", + ".....................", + "....................×", + "..............×××.×.." ], "graph": [ "Edges:", @@ -82,22 +118,60 @@ " 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", + "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", "decoded": "+: STAR.TIME | ONE | OPEN.COUNT\n×: _ | _ | COUNT.12\n" }, + "numbers.txt": { + "rows_one_per_line": [ + "+.+....................+.", + "+.+..+....+...........+.+", + "+.+...........+++.+....++", + ".........................", + ".........................", + ".........................", + ".+..+.+..................", + "+++.+.+...+.........+....", + ".+..+.+.......+++.+......" + ], + "source": "# \"Now is 2219. Twelve pulsars.\" Big numbers are base-512 digits after COUNT.\n# COUNT.4.171 = 4 x 512 + 171 = 2219 (you can also write COUNT.2219).\n+: TIME.ONE | ONE | COUNT.4.171\n\n+: STAR.TIME | ONE | COUNT.12\n", + "formatted": "+: TIME.ONE | ONE | COUNT.4.171\n\n+: STAR.TIME | ONE | COUNT.12\n", + "rows": [ + "+.+....................+......+..+.+..............", + "+.+..+....+...........+.+....+++.+.+...+.........+", + "+.+...........+++.+....++.....+..+.+.......+++.+.." + ], + "graph": [ + "Edges:", + "1. (+) now --is, equals--> 2219", + "2. (+) pulsar --is, equals--> 12", + "", + "Nodes:", + " TIME.ONE now", + " COUNT.4.171 2219", + " 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", + "decoded": "+: TIME.ONE | ONE | COUNT.4.171\n\n+: STAR.TIME | ONE | 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", + "rows_one_per_line": [ + "+++..+.+.......+..+.+", + "+.+...+...+...+++.+.+", + "+++..+.+.......+..+.+", + ".....................", + ".....................", + ".....................", + ".+..+.+..............", + "+++.+.+...+.........+", + ".+..+.+.......+++.+.." + ], + "source": "# \"We see pulsars. The pulsars are twelve.\"\n# The same word in two triplets 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:", @@ -109,20 +183,29 @@ " 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", + "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", "decoded": "+: SELF | LIGHT.ONE | STAR.TIME\n\n+: STAR.TIME | ONE | COUNT.12\n" }, "question.txt": { + "rows_one_per_line": [ + "+++.+++........+.....", + "+++.+.....+...+.+....", + "+++.+++........+.....", + ".....................", + "..............×××.×××", + "..............×××....", + "..............×××.×××" + ], "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:", @@ -133,43 +216,53 @@ " 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", + "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", "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", + "rows_one_per_line": [ + "+++.+++.......+++.+++", + "+++.+.....+...+++....", + "+++.+++.......+++.+++", + ".....................", + ".....................", + ".....................", + "+++..+.+.+...........", + "+.+...+..+++...+.....", + "+++..+.+.+..........." + ], + "source": "# \"Your world is water. We saw that.\"\n# A lone ONE in a node slot is \"that\": the triplet before.\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]", + "2. (+) we --see [past]--> [triplet 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", + "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", "decoded": "+: BODY.OTHER | ONE | BODY.WATER\n\n+: SELF | LIGHT.BEFORE | ONE\n" }, "saw-your-world.txt": { + "rows_one_per_line": [ + "+++..+.+.+....+++.+++", + "+.+...+..+++..+++.+..", + "+++..+.+.+....+++.+++" + ], "source": "# \"We saw your world.\"\n+: SELF | LIGHT.BEFORE | BODY.OTHER\n", "formatted": "+: SELF | LIGHT.BEFORE | BODY.OTHER\n", "rows": [ - "+++.....+.+.....+++.+++", - "+.+......+..+...+++.+..", - "+++.....+.+.....+++.+++" + "+++..+.+.+....+++.+++", + "+.+...+..+++..+++.+..", + "+++..+.+.+....+++.+++" ], "graph": [ "Edges:", @@ -179,22 +272,27 @@ " 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", + "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", "decoded": "+: SELF | LIGHT.BEFORE | BODY.OTHER\n" }, "shadow.txt": { + "rows_one_per_line": [ + "+++..+.+.+....+++", + "+.+...+..+++..+..", + "+++..+.+.+....+++", + ".................", + ".................", + ".................", + "+++..+.+.+....+++", + "+.+...+...+...+..", + "+++..+.+...+..+++" + ], "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:", @@ -205,24 +303,37 @@ " 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", + "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", "decoded": "+: SELF | LIGHT.BEFORE | OTHER\n\n+: SELF | LIGHT.NOT | OTHER\n" }, "third-voice.txt": { + "rows_one_per_line": [ + "+++.+++.......+++.+.+", + "+++.+.....+...+++....", + "+++.+++.......+++.+.+", + ".....................", + "..................×××", + ".....................", + "..................×××", + ".....................", + "..................*.*", + ".....................", + "..................*.*" + ], "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:", @@ -234,24 +345,24 @@ " 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", + "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", "decoded": "+: BODY.OTHER | ONE | BODY.AIR\n×: _ | _ | _.WATER\n*: _ | _ | _.AIR\n" } }, "words": { "SELF": { "rows": [ - "+++....", - "+.+....", - "+++...." + "+++", + "+.+", + "+++" ], "gloss": "we" }, "OTHER": { "rows": [ - "+++....", - "+......", - "+++...." + "+++", + "+..", + "+++" ], "gloss": "you" }, @@ -297,9 +408,9 @@ }, "STAR": { "rows": [ - ".+.....", - "+++....", - ".+....." + ".+.", + "+++", + ".+." ], "gloss": "star" }, @@ -361,9 +472,9 @@ }, "BODY": { "rows": [ - "+++....", - "+++....", - "+++...." + "+++", + "+++", + "+++" ], "gloss": "world, matter" }, @@ -417,9 +528,9 @@ }, "LIGHT": { "rows": [ - "+.+....", - ".+.....", - "+.+...." + "+.+", + ".+.", + "+.+" ], "gloss": "light" }, @@ -441,9 +552,9 @@ }, "AIR": { "rows": [ - "+.+....", - ".......", - "+.+...." + "+.+", + "...", + "+.+" ], "gloss": "air" }, @@ -465,9 +576,9 @@ }, "WATER": { "rows": [ - "+++....", - ".......", - "+++...." + "+++", + "...", + "+++" ], "gloss": "water" }, @@ -497,17 +608,17 @@ }, "TIME": { "rows": [ - "+.+....", - "+.+....", - "+.+...." + "+.+", + "+.+", + "+.+" ], "gloss": "time" }, "TIME.BEFORE": { "rows": [ - "+.+....", "+.+.+..", - "+.+...." + "+.+.+++", + "+.+.+.." ], "gloss": "the past, before" }, @@ -521,9 +632,9 @@ }, "TIME.AFTER": { "rows": [ - "+.+....", "+.+...+", - "+.+...." + "+.+.+++", + "+.+...+" ], "gloss": "the future, after" }, @@ -569,9 +680,9 @@ }, "VOICE": { "rows": [ - "+......", - "++.....", - "+++...." + "+..", + "++.", + "+++" ], "gloss": "sound, voice" }, @@ -641,9 +752,9 @@ }, "ONE": { "rows": [ - ".......", - ".+.....", - "......." + "...", + ".+.", + "..." ], "gloss": "that" }, @@ -673,9 +784,9 @@ }, "OPEN": { "rows": [ - ".+.....", - "+.+....", - ".+....." + ".+.", + "+.+", + ".+." ], "gloss": "what?" } @@ -740,7 +851,9 @@ "error": "unknown part: WIND (see `glyph parts`)" }, "COUNT.512": { - "error": "number out of range 0-511: 512" + "word": "COUNT.1.0", + "node": "512", + "relation": "number [1.0]" }, "STAR.12": { "error": "a number needs COUNT as its kind, e.g. COUNT.12 (got STAR.12)" @@ -758,13 +871,47 @@ "word": "_.0", "node": "_ | 0 (not in vocabulary)", "relation": " [0]" + }, + "COUNT.4.171": { + "word": "COUNT.4.171", + "node": "2219", + "relation": "number [4.171]" + }, + "COUNT.2219": { + "word": "COUNT.4.171", + "node": "2219", + "relation": "number [4.171]" + }, + "COUNT.70491": { + "word": "COUNT.137.347", + "node": "70491", + "relation": "number [137.347]" + }, + "COUNT.12.106.0": { + "word": "COUNT.12.106.0", + "node": "3200000", + "relation": "number [12.106.0]" + }, + "COUNT.4.512": { + "error": "each digit of a number is 0-511 (base 512): COUNT.4.512" + }, + "COUNT.0.5": { + "error": "a number does not start with a 0 digit: COUNT.0.5" + }, + "_.4.170": { + "word": "_.4.170", + "node": "_ | 4.170 (not in vocabulary)", + "relation": " [4.170]" + }, + "BODY.SELF.OTHER": { + "error": "only numbers take more than two positions: BODY.SELF.OTHER" } }, "numbers": { "0": [ - ".......", - ".......", - "+++...." + "...", + "...", + "+++" ], "1": [ ".......", @@ -3320,6 +3467,36 @@ "....+++", "....+++", "+++.+++" + ], + "512": [ + "...........", + "...........", + "+++...+...." + ], + "2219": [ + ".........+.", + "........+.+", + "+++.+....++" + ], + "70491": [ + ".....+..+.+", + "......+..++", + "+++...+..++" + ], + "262143": [ + "....+++.+++", + "....+++.+++", + "+++.+++.+++" + ], + "262144": [ + "...............", + "...............", + "+++...+........" + ], + "3200000": [ + "..........+....", + "......+.+.+....", + "+++.+....+....." ] } } diff --git a/tests/glyph-examples/conversation.txt b/tests/glyph-examples/conversation.txt index 73871ea..5b4339a 100644 --- a/tests/glyph-examples/conversation.txt +++ b/tests/glyph-examples/conversation.txt @@ -1,4 +1,4 @@ -# They speak: three statements. A blank line starts the next edge. +# They speak: three triplets. A blank line starts the next one. +: BODY.OTHER | ONE | BODY.AIR ×: _ | _ | _.WATER *: _ | _ | _.AIR diff --git a/tests/glyph-examples/how-many.txt b/tests/glyph-examples/how-many.txt index 6f8e67b..dd3c4c1 100644 --- a/tests/glyph-examples/how-many.txt +++ b/tests/glyph-examples/how-many.txt @@ -1,4 +1,4 @@ # "How many pulsars?" Twelve. -# OPEN.COUNT asks "what number?"; the answer COUNT.12 replaces OPEN with COUNT, and COUNT with the bits. +# OPEN.COUNT asks "what number?"; the answer COUNT.12 replaces OPEN with COUNT, and COUNT with the digit. +: STAR.TIME | ONE | OPEN.COUNT ×: _ | _ | COUNT.12 diff --git a/tests/glyph-examples/numbers.txt b/tests/glyph-examples/numbers.txt new file mode 100644 index 0000000..824636f --- /dev/null +++ b/tests/glyph-examples/numbers.txt @@ -0,0 +1,5 @@ +# "Now is 2219. Twelve pulsars." Big numbers are base-512 digits after COUNT. +# COUNT.4.171 = 4 x 512 + 171 = 2219 (you can also write COUNT.2219). ++: TIME.ONE | ONE | COUNT.4.171 + ++: STAR.TIME | ONE | COUNT.12 diff --git a/tests/glyph-examples/pulsars.txt b/tests/glyph-examples/pulsars.txt index 32e7099..0f0c579 100644 --- a/tests/glyph-examples/pulsars.txt +++ b/tests/glyph-examples/pulsars.txt @@ -1,5 +1,5 @@ # "We see pulsars. The pulsars are twelve." -# The same word on two lines is one node with two edges. +# The same word in two triplets is one node with two edges. +: SELF | LIGHT.ONE | STAR.TIME +: STAR.TIME | ONE | COUNT.12 diff --git a/tests/glyph-examples/saw-that.txt b/tests/glyph-examples/saw-that.txt index 3d22412..1aafe0b 100644 --- a/tests/glyph-examples/saw-that.txt +++ b/tests/glyph-examples/saw-that.txt @@ -1,5 +1,5 @@ # "Your world is water. We saw that." -# A lone ONE in a node slot is "that": the statement on the line above. +# A lone ONE in a node slot is "that": the triplet before. +: BODY.OTHER | ONE | BODY.WATER +: SELF | LIGHT.BEFORE | ONE diff --git a/tests/glyph.test.mjs b/tests/glyph.test.mjs index c424a2d..a80859c 100644 --- a/tests/glyph.test.mjs +++ b/tests/glyph.test.mjs @@ -5,7 +5,7 @@ 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, + numberShape, wordStr, word, bandRows, responseText, positions, value, toDigits, hiddenZeros, } from '../lib/glyph.js'; const read = (p) => readFileSync(new URL(p, import.meta.url), 'utf8'); @@ -25,6 +25,7 @@ for (const [name, ex] of Object.entries(CLI.examples)) { const p = page(ex.source); assert.equal(formatPage(p), ex.formatted); assert.deepEqual(render(vocab, p), ex.rows); + assert.deepEqual(render(vocab, p, 1), ex.rows_one_per_line); 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); @@ -54,41 +55,55 @@ test('parsing words matches glyph-cli', () => { } }); -test('numbers are nine bits, as in glyph-cli', () => { +test('numbers are base-512 digits of nine bits, as in glyph-cli', () => { + assert.deepEqual(toDigits(2219), [4, 171]); + assert.equal(value(vocab.parse('COUNT.4.171')), 2219); + assert.deepEqual(positions(vocab.parse('COUNT.4.171')), ['COUNT', 4, 171]); + assert.deepEqual(positions(vocab.parse('SELF')), ['SELF']); 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('/'))); + const words = new Set(vocab.entries.map((e) => bandRows(vocab, positions(e.word), '#').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')); +test('triplets are padded to the page voices and spaced 1, 2, 4', () => { + const p = page('+: SELF | ONE | OTHER\n×: _ | _ | _.NOT\n\n+: SELF | ONE | SELF.OTHER'); + const rows = render(vocab, p, 1); assert.equal(rows.length, 7 + 3 + 7); - assert.ok(rows.every((r) => [...r].length === WIDTH)); + assert.equal(new Set(rows.map((r) => r.length)).size, 1); + assert.deepEqual(render(vocab, p)[0], '+++.......+++........+++.......+++.+++'); }); 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}`); + for (const n of [0, 1, 12, 16, 137, 170, 273, 487, 495, 511, 513, 2219, 70491, 262143, 3200001]) { + for (const where of [`STAR.TIME | ONE | COUNT.${n}`, `COUNT.${n} | ONE | STAR.TIME`]) { + const p = page(`+: ${where}`); + assert.deepEqual(decode(vocab, render(vocab, p).join('\n')), p, where); + } } 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); + for (const src of ['+: ONE.SELF | ONE | ONE.OTHER', '+: BODY.AFTER | ONE | OTHER', '+: BODY | ONE.OTHER | OTHER', '+: SELF | VOICE.ONE | ONE']) { + const q = page(src); + assert.deepEqual(decode(vocab, render(vocab, q).join('\n')), q, src); + } +}); + +test('a 0 digit at the end of a line is flagged', () => { + assert.deepEqual(hiddenZeros(page('+: SELF | LIGHT | COUNT.12.106.0')), [1]); + assert.deepEqual(hiddenZeros(page('+: SELF | LIGHT | COUNT.12.106.0\n\n+: SELF | VOICE | ONE')), []); }); 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); + assert.throws(() => decode(vocab, '+++\n...\n'), /lines and bands/); + assert.throws(() => decode(vocab, '+++++++\n+.+.+.+\n+++++++\n'), /1, 2 and 4 cell gaps/); + assert.throws(() => decode(vocab, '+++.......×××\n+.+...+...×.×\n+++.......×××\n'), /mixes symbols/); + assert.throws(() => decode(vocab, '+.+..........\n.............\n.............\n'), /1, 2 and 4 cell gaps/); + assert.equal(decode(vocab, '').triplets.length, 0); }); test('parse errors name the line', () => { @@ -111,7 +126,9 @@ test('rule five', () => { 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]'); + assert.equal(formatGraph(vocab, g)[1], '1. (+) we --see [past]--> [that: nothing before]'); + const d = readGraph(vocab, page('+: SELF | LIGHT | COUNT.4.171\n×: _ | _ | _.4.170')); + assert.deepEqual(d.edges[0].replies[0].responses.map((r) => responseText(r)), ['object.digit 1: yes (to +)', 'object.digit 2: instead "170" (to +)']); }); test('the Chinese glosses cover the whole vocabulary', () => { From 6bf347e1f54070d33b5bd9b4c9c74a26e2b08647 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 05:38:53 +0000 Subject: [PATCH 5/5] Grid script: keep BEFORE and AFTER as single dots (L's call) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QgskKyin9t8k9F6KcUj5o7 --- lib/glyph-vocab.json | 8 ++--- lib/glyph.js | 12 ++----- tests/glyph-cli-fixtures.json | 64 +++++++++++++++++------------------ tests/glyph.test.mjs | 2 +- 4 files changed, 39 insertions(+), 47 deletions(-) diff --git a/lib/glyph-vocab.json b/lib/glyph-vocab.json index 4cc67bf..0fcf300 100644 --- a/lib/glyph-vocab.json +++ b/lib/glyph-vocab.json @@ -111,18 +111,18 @@ }, "BEFORE": { "shape": [ + "...", "#..", - "###", - "#.." + "..." ], "thing": "earlier", "relation": null }, "AFTER": { "shape": [ + "...", "..#", - "###", - "..#" + "..." ], "thing": "later", "relation": null diff --git a/lib/glyph.js b/lib/glyph.js index 64584ee..c216a34 100644 --- a/lib/glyph.js +++ b/lib/glyph.js @@ -195,14 +195,6 @@ export class Vocabulary { shapes.set(key, p.name); if (!p.shape.some((r) => r.includes(MARK))) errors.push(`part ${p.name}: shape is empty`); } - for (const p of Object.values(this.parts)) { - // Spacing is structure: a part moved one cell sideways must not be another part, - // or a 1-cell gap could read as a 2-cell gap. - for (const moved of [p.shape.map((r) => r.slice(1) + '.'), p.shape.map((r) => '.' + r.slice(0, 2))]) { - const other = shapes.get(moved.join('/')); - if (other && other !== p.name) errors.push(`part ${p.name} moved one cell sideways is ${other}: gaps would be ambiguous`); - } - } const seen = new Map(); const glosses = new Map(); for (const e of this.entries) { @@ -431,8 +423,8 @@ const bits = (pattern) => parseInt(pattern.join('').replaceAll(EMPTY_CELL, '0'). /** * Split one drawn line into triplets and words. The gaps say where everything is: 1 empty column inside - * a word, 2 between words, 4 between triplets. Number digits can have empty columns of their own, so the - * reader finds every layout in which each position is a part (or a digit, in a number) and keeps the one + * a word, 2 between words, 4 between triplets. ONE, BEFORE, AFTER and number digits have empty columns of + * their own (ONE moved one cell left is BEFORE), so the reader finds every layout in which each position is a part (or a digit, in a number) and keeps the one * whose statements read best against the vocabulary. */ class LineReader { diff --git a/tests/glyph-cli-fixtures.json b/tests/glyph-cli-fixtures.json index 7401943..2187716 100644 --- a/tests/glyph-cli-fixtures.json +++ b/tests/glyph-cli-fixtures.json @@ -17,9 +17,9 @@ ".....................", ".....................", ".....................", - "+++..+.+.+....+++.+++", - "+.+...+..+++..+++.+..", - "+++..+.+.+....+++.+++", + "+++..+.+......+++.+++", + "+.+...+..+....+++.+..", + "+++..+.+......+++.+++", ".....................", ".....................", ".....................", @@ -46,9 +46,9 @@ "source": "# They speak: three triplets. A blank line starts the next one.\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": [ - "+++.+++.......+++.+.+....+++..+.+.+....+++.+++", - "+++.+.....+...+++........+.+...+..+++..+++.+..", - "+++.+++.......+++.+.+....+++..+.+.+....+++.+++", + "+++.+++.......+++.+.+....+++..+.+......+++.+++", + "+++.+.....+...+++........+.+...+..+....+++.+..", + "+++.+++.......+++.+.+....+++..+.+......+++.+++", "..............................................", "..................×××.........................", "..............................................", @@ -85,7 +85,7 @@ " 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\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", + "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\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\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": { @@ -227,16 +227,16 @@ ".....................", ".....................", ".....................", - "+++..+.+.+...........", - "+.+...+..+++...+.....", - "+++..+.+.+..........." + "+++..+.+.............", + "+.+...+..+.....+.....", + "+++..+.+............." ], "source": "# \"Your world is water. We saw that.\"\n# A lone ONE in a node slot is \"that\": the triplet before.\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:", @@ -248,21 +248,21 @@ " 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", + "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", "decoded": "+: BODY.OTHER | ONE | BODY.WATER\n\n+: SELF | LIGHT.BEFORE | ONE\n" }, "saw-your-world.txt": { "rows_one_per_line": [ - "+++..+.+.+....+++.+++", - "+.+...+..+++..+++.+..", - "+++..+.+.+....+++.+++" + "+++..+.+......+++.+++", + "+.+...+..+....+++.+..", + "+++..+.+......+++.+++" ], "source": "# \"We saw your world.\"\n+: SELF | LIGHT.BEFORE | BODY.OTHER\n", "formatted": "+: SELF | LIGHT.BEFORE | BODY.OTHER\n", "rows": [ - "+++..+.+.+....+++.+++", - "+.+...+..+++..+++.+..", - "+++..+.+.+....+++.+++" + "+++..+.+......+++.+++", + "+.+...+..+....+++.+..", + "+++..+.+......+++.+++" ], "graph": [ "Edges:", @@ -272,14 +272,14 @@ " 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", + "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", "decoded": "+: SELF | LIGHT.BEFORE | BODY.OTHER\n" }, "shadow.txt": { "rows_one_per_line": [ - "+++..+.+.+....+++", - "+.+...+..+++..+..", - "+++..+.+.+....+++", + "+++..+.+......+++", + "+.+...+..+....+..", + "+++..+.+......+++", ".................", ".................", ".................", @@ -290,9 +290,9 @@ "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:", @@ -303,7 +303,7 @@ " 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", + "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", "decoded": "+: SELF | LIGHT.BEFORE | OTHER\n\n+: SELF | LIGHT.NOT | OTHER\n" }, "third-voice.txt": { @@ -616,9 +616,9 @@ }, "TIME.BEFORE": { "rows": [ + "+.+....", "+.+.+..", - "+.+.+++", - "+.+.+.." + "+.+...." ], "gloss": "the past, before" }, @@ -632,9 +632,9 @@ }, "TIME.AFTER": { "rows": [ + "+.+....", "+.+...+", - "+.+.+++", - "+.+...+" + "+.+...." ], "gloss": "the future, after" }, diff --git a/tests/glyph.test.mjs b/tests/glyph.test.mjs index a80859c..31c37c2 100644 --- a/tests/glyph.test.mjs +++ b/tests/glyph.test.mjs @@ -87,7 +87,7 @@ test('numbers read back as numbers', () => { } const p = page('+: STAR.TIME | ONE | COUNT.12\n×: _ | _ | _.495'); assert.deepEqual(decode(vocab, render(vocab, p).join('\n')), p); - for (const src of ['+: ONE.SELF | ONE | ONE.OTHER', '+: BODY.AFTER | ONE | OTHER', '+: BODY | ONE.OTHER | OTHER', '+: SELF | VOICE.ONE | ONE']) { + for (const src of ['+: ONE.SELF | ONE | ONE.OTHER', '+: BODY | ONE.OTHER | OTHER', '+: SELF | VOICE.ONE | ONE']) { const q = page(src); assert.deepEqual(decode(vocab, render(vocab, q).join('\n')), q, src); }
        下面画的意思