oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1// The Markdown subset the operations accept text in — core/src/markdown.rs
2// line for line, because a batch applied here and the same batch applied
3// through the core must produce the same runs.
4//
5// - Paragraphs split on blank lines; the lines of one paragraph join with
6// a space.
7// - A line starting `- ` or `1. ` (any digits) is its own paragraph with
8// the marker stripped.
9// - Inline: **bold**, *italic*, `code`, ==highlight==, [text](url). A
10// delimiter with no closing partner is literal text. Inside a code span
11// nothing else is parsed. A link's text may carry marks; a link inside a
12// link is literal.
13// - The only escape is \*, which yields a literal *.
14//
15// A run is {t, b?, i?, c?, h?, href?}: the view's run shape, false and
16// absent marks omitted. parseLines is the plain variant for code blocks.
17
18export function sameMarks (a, b) {
19 return !!a.b === !!b.b && !!a.i === !!b.i && !!a.c === !!b.c && !!a.h === !!b.h &&
20 (a.href ?? null) === (b.href ?? null)
21}
22
23/** A run with only the marks that are on, so deepEqual sees the view shape. */
24export function run (t, marks = {}) {
25 const r = { t }
26 if (marks.b) r.b = true
27 if (marks.i) r.i = true
28 if (marks.c) r.c = true
29 if (marks.h) r.h = true
30 if (marks.href != null) r.href = marks.href
31 return r
32}
33
34/** Appends a run to a paragraph, merging into the last one when the marks match. */
35export function pushRun (paragraph, r) {
36 const last = paragraph[paragraph.length - 1]
37 if (last && sameMarks(last, r)) last.t += r.t
38 else paragraph.push(r)
39}
40
41/** Rust's str::lines(): split on \n or \r\n, the final line ending optional, no phantom last line. */
42export function lines (input) {
43 const out = []
44 let start = 0
45 while (start < input.length) {
46 const nl = input.indexOf('\n', start)
47 if (nl === -1) { out.push(input.slice(start)); break }
48 const line = input.slice(start, nl)
49 out.push(line.endsWith('\r') ? line.slice(0, -1) : line)
50 start = nl + 1
51 }
52 return out
53}
54
55export function parse (input) {
56 const sources = []
57 let current = null
58 for (let line of lines(input)) {
59 line = line.trim()
60 if (line === '') {
61 if (current !== null) { sources.push(current); current = null }
62 continue
63 }
64 const item = listItem(line)
65 if (item !== null) {
66 if (current !== null) sources.push(current)
67 current = item
68 continue
69 }
70 current = current === null ? line : current + ' ' + line
71 }
72 if (current !== null) sources.push(current)
73 return sources.map(parseInline)
74}
75
76export function parseLines (input) {
77 return lines(input).map(line => (line === '' ? [] : [run(line)]))
78}
79
80function listItem (line) {
81 if (line.startsWith('- ')) return line.slice(2).trimStart()
82 let digits = 0
83 while (digits < line.length && line.charCodeAt(digits) >= 48 && line.charCodeAt(digits) <= 57) digits++
84 if (digits > 0 && line.startsWith('. ', digits)) return line.slice(digits + 2).trimStart()
85 return null
86}
87
88class Parser {
89 constructor () {
90 this.runs = []
91 this.buf = ''
92 this.marks = { b: false, i: false, c: false, h: false, href: null }
93 }
94
95 flush () {
96 if (this.buf === '') return
97 pushRun(this.runs, run(this.buf, this.marks))
98 this.buf = ''
99 }
100
101 toggle (delim, rest, key) {
102 if (this.marks[key]) {
103 this.flush()
104 this.marks[key] = false
105 return true
106 }
107 if (hasPartner(rest, delim)) {
108 this.flush()
109 this.marks[key] = true
110 return true
111 }
112 return false
113 }
114
115 parse (src) {
116 let i = 0
117 while (i < src.length) {
118 const rest = src.slice(i)
119 if (rest.startsWith('\\*')) { this.buf += '*'; i += 2; continue }
120 if (rest.startsWith('**')) {
121 if (this.toggle('**', rest.slice(2), 'b')) { i += 2; continue }
122 } else if (rest.startsWith('==')) {
123 if (this.toggle('==', rest.slice(2), 'h')) { i += 2; continue }
124 } else if (rest.startsWith('*')) {
125 if (this.toggle('*', rest.slice(1), 'i')) { i += 1; continue }
126 } else if (rest.startsWith('`')) {
127 const end = rest.indexOf('`', 1)
128 if (end !== -1) {
129 this.flush()
130 const was = this.marks.c
131 this.marks.c = true
132 this.buf += rest.slice(1, end)
133 this.flush()
134 this.marks.c = was
135 i += end + 1
136 continue
137 }
138 } else if (rest.startsWith('[') && this.marks.href === null) {
139 const l = link(rest)
140 if (l !== null) {
141 this.flush()
142 const outer = { ...this.marks }
143 this.marks.href = l.url
144 this.parse(l.text)
145 this.flush()
146 this.marks = outer
147 i += l.len
148 continue
149 }
150 }
151 const ch = String.fromCodePoint(rest.codePointAt(0))
152 this.buf += ch
153 i += ch.length
154 }
155 }
156}
157
158/** Whether delim occurs in rest as itself: for a lone *, one that is not half of a **. */
159function hasPartner (rest, delim) {
160 if (delim !== '*') return rest.includes(delim)
161 let i = 0
162 while (i < rest.length) {
163 if (rest[i] === '*') {
164 if (rest[i + 1] === '*') { i += 2; continue }
165 return true
166 }
167 i++
168 }
169 return false
170}
171
172/** [text](url) at the start of s: the text, the url, and the length the whole link spans. */
173function link (s) {
174 const close = s.indexOf('](')
175 if (close === -1) return null
176 const text = s.slice(1, close)
177 const urlStart = close + 2
178 const end = s.indexOf(')', urlStart)
179 if (end === -1) return null
180 const url = s.slice(urlStart, end)
181 if (text === '' || url === '') return null
182 return { text, url, len: end + 1 }
183}
184
185function parseInline (src) {
186 const p = new Parser()
187 p.parse(src)
188 p.flush()
189 return p.runs
190}
191