oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1// fixtures/poison.mjs — documents no op can produce: the malformed
2// shapes a peer could put into the CRDT by hand (box.x = "a",
3// background.kind = 5, fonts.body = [], an asset that is a string),
4// built directly with yjs. view.test.mjs runs the view over them and
5// core/src/view.rs's tests build the same shapes with yrs; run as a
6// script this writes each one as a Yjs v1 update into
7// internal/slidestest/testdata/poison/, which
8// TestPoisonUpdateDoesNotBrickTheDeck posts at the real server.
9//
10// Every document has a fixed guid and client id, so a re-run writes the
11// same bytes and the committed files only move when this file does.
12// Each update adds a slide of its own (or touches meta/assets), so it
13// applies cleanly to any deck.
14import * as Y from 'yjs'
15import { mkdirSync, writeFileSync } from 'node:fs'
16import { join, dirname } from 'node:path'
17import { fileURLToPath } from 'node:url'
18
19const here = dirname(fileURLToPath(import.meta.url))
20export const poisonDir = join(here, '..', 'internal', 'slidestest', 'testdata', 'poison')
21
22function slideWith (doc, id, fill) {
23 const slide = new Y.Map()
24 doc.getArray('slides').push([slide])
25 slide.set('id', id)
26 slide.set('layout', 'canvas')
27 slide.set('notes', new Y.XmlFragment())
28 const blocks = new Y.Array()
29 slide.set('blocks', blocks)
30 fill(slide, blocks)
31}
32
33function blockWith (blocks, id, fill) {
34 const block = new Y.Map()
35 blocks.push([block])
36 block.set('id', id)
37 block.set('type', 'text')
38 block.set('props', new Y.Map())
39 block.set('phone', new Y.Map())
40 block.set('z', 0)
41 block.set('text', new Y.XmlFragment())
42 fill(block)
43}
44
45/** The poison shapes, name → a fill function over a fresh Y.Doc. */
46export const shapes = {
47 // A canvas block whose box is not four numbers.
48 box: doc => slideWith(doc, 'poisonboxsld', (slide, blocks) => {
49 blockWith(blocks, 'poisonboxblk', block => block.set('box', { x: 'a', y: 0, w: 1, h: 1 }))
50 }),
51 // A slide whose background kind is a number.
52 background: doc => slideWith(doc, 'poisonbgsld1', slide => {
53 const bg = new Y.Map()
54 slide.set('background', bg)
55 bg.set('kind', 5)
56 bg.set('value', '')
57 }),
58 // meta.fonts with a body that is an array.
59 fonts: doc => { doc.getMap('meta').set('fonts', { heading: 'Serif', body: [] }) },
60 // An assets entry that is a string, one with no ref, one with a
61 // mistyped name.
62 assets: doc => {
63 const assets = doc.getMap('assets')
64 assets.set('poisonstr', 'not a map')
65 assets.set('poisonnoref', { name: 'x.png' })
66 assets.set('poisonbadnm', { ref: 'poisonref', name: 3, width: 'w', height: 9.9, alt: null })
67 },
68 // A phone order the view passes (it is a number) and the server's
69 // typed reader cannot hold: what the parse gate in appendUpdate
70 // exists for, over and above the view's own tolerance.
71 magnitude: doc => slideWith(doc, 'poisonmagsld', (slide, blocks) => {
72 blockWith(blocks, 'poisonmagblk', block => {
73 block.set('region', 'body')
74 block.get('phone').set('order', 1e300)
75 })
76 }),
77 // A run under a formatting attribute the schema does not define
78 // (codex 3). The view reads the words fine, so the typed parse
79 // passes; y-prosemirror 1.3.7 deletes the XmlText when it binds. The
80 // gate refuses it by checking the shared text, not the projection
81 // (core/src/check.rs). Last in this list, so the client ids of the
82 // shapes before it — and their committed bytes — do not move.
83 unknownmark: doc => slideWith(doc, 'poisonmrksld', (slide, blocks) => {
84 blockWith(blocks, 'poisonmrkblk', block => {
85 block.set('region', 'body')
86 const p = new Y.XmlElement('paragraph')
87 const t = new Y.XmlText()
88 p.insert(0, [t])
89 block.get('text').insert(0, [p])
90 t.applyDelta([{ insert: 'keep ' }, { insert: 'these words', attributes: { unknown_mark: true } }])
91 })
92 })
93}
94
95/** A fresh document holding one poison shape. */
96export function poisonDoc (name) {
97 const fill = shapes[name]
98 if (!fill) throw new Error(`no poison shape ${name}`)
99 const doc = new Y.Doc({ guid: `poison-${name}` })
100 doc.clientID = 900 + Object.keys(shapes).indexOf(name)
101 doc.transact(() => fill(doc))
102 return doc
103}
104
105/** Every shape as a Yjs v1 update, name → bytes. */
106export function poisonUpdates () {
107 const out = {}
108 for (const name of Object.keys(shapes)) out[name] = Y.encodeStateAsUpdate(poisonDoc(name))
109 return out
110}
111
112export function writePoison () {
113 mkdirSync(poisonDir, { recursive: true })
114 for (const [name, bytes] of Object.entries(poisonUpdates())) {
115 writeFileSync(join(poisonDir, `${name}.yjs`), Buffer.from(bytes))
116 console.log(`${name}: ${bytes.length} bytes`)
117 }
118}
119
120if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) writePoison()
121