ntk 7.6.1 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,148 +0,0 @@
1
- // Markdown parsing for MarkdownView — a thin adapter over the `marked`
2
- // lexer (CommonMark + GFM), mapping its token stream onto the small block /
3
- // inline AST the widget renders. The parser used to be hand-rolled here;
4
- // it was replaced after subtle divergences from CommonMark surfaced
5
- // (e.g. intra-word `WM_DELETE_WINDOW` sprouting emphasis).
6
- //
7
- // block: { type: 'heading', level, children }
8
- // | { type: 'paragraph', children }
9
- // | { type: 'code', text, lang }
10
- // | { type: 'blockquote', blocks }
11
- // | { type: 'list', ordered, start, items: [ blocks ] }
12
- // | { type: 'hr' }
13
- // | { type: 'table', align: ['left'|'center'|'right'],
14
- // header: [ inline[] ], rows: [ [ inline[] ] ] }
15
- // inline: { type: 'text', text }
16
- // | { type: 'strong'|'em', children }
17
- // | { type: 'code', text }
18
- // | { type: 'link', children, href }
19
-
20
- import { Lexer } from 'marked';
21
-
22
- export function parseMarkdown(src) {
23
- return mapBlocks(new Lexer({ gfm: true }).lex(String(src ?? '')));
24
- }
25
-
26
- /** inline-only parsing (exported for tests / embedders) */
27
- export function parseInline(text) {
28
- return mapInline(Lexer.lexInline(String(text ?? '')));
29
- }
30
-
31
- function mapBlocks(tokens) {
32
- const blocks = [];
33
- for (const t of tokens) {
34
- switch (t.type) {
35
- case 'heading':
36
- blocks.push({ type: 'heading', level: t.depth, children: mapInline(t.tokens) });
37
- break;
38
- case 'paragraph':
39
- // tight list items carry their inline content in a block-level 'text'
40
- case 'text':
41
- blocks.push({ type: 'paragraph', children: mapInline(t.tokens ?? [t]) });
42
- break;
43
- case 'code':
44
- blocks.push({
45
- type: 'code',
46
- text: t.text,
47
- lang: (t.lang || '').split(/\s+/)[0]
48
- });
49
- break;
50
- case 'blockquote':
51
- blocks.push({ type: 'blockquote', blocks: mapBlocks(t.tokens) });
52
- break;
53
- case 'list':
54
- blocks.push({
55
- type: 'list',
56
- ordered: !!t.ordered,
57
- start: typeof t.start === 'number' ? t.start : 1,
58
- items: t.items.map((item) => mapBlocks(item.tokens))
59
- });
60
- break;
61
- case 'hr':
62
- blocks.push({ type: 'hr' });
63
- break;
64
- case 'table':
65
- blocks.push({
66
- type: 'table',
67
- align: t.align.map((a) => a ?? 'left'),
68
- header: t.header.map((cell) => mapInline(cell.tokens)),
69
- rows: t.rows.map((row) => row.map((cell) => mapInline(cell.tokens)))
70
- });
71
- break;
72
- case 'html':
73
- blocks.push({ type: 'paragraph', children: [{ type: 'text', text: t.text.trim() }] });
74
- break;
75
- case 'space':
76
- case 'def':
77
- break;
78
- default:
79
- if (t.tokens) blocks.push({ type: 'paragraph', children: mapInline(t.tokens) });
80
- else if (t.text) blocks.push({ type: 'paragraph', children: [{ type: 'text', text: t.text }] });
81
- break;
82
- }
83
- }
84
- return blocks;
85
- }
86
-
87
- function mapInline(tokens) {
88
- const nodes = [];
89
- const pushText = (text) => {
90
- // soft line breaks inside a paragraph render as spaces (the layout
91
- // engine treats \n as a hard break)
92
- text = text.replace(/\n/g, ' ');
93
- const last = nodes[nodes.length - 1];
94
- if (last && last.type === 'text') last.text += text;
95
- else nodes.push({ type: 'text', text });
96
- };
97
- for (const t of tokens ?? []) {
98
- switch (t.type) {
99
- case 'text':
100
- if (t.tokens) nodes.push(...mapInline(t.tokens));
101
- else pushText(t.escaped ? unescapeHtml(t.text) : t.text);
102
- break;
103
- case 'escape':
104
- pushText(t.text);
105
- break;
106
- case 'strong':
107
- nodes.push({ type: 'strong', children: mapInline(t.tokens) });
108
- break;
109
- case 'em':
110
- nodes.push({ type: 'em', children: mapInline(t.tokens) });
111
- break;
112
- case 'del':
113
- // no strikethrough decoration (yet): render the content plainly
114
- nodes.push(...mapInline(t.tokens));
115
- break;
116
- case 'codespan':
117
- nodes.push({ type: 'code', text: t.text });
118
- break;
119
- case 'link':
120
- nodes.push({ type: 'link', children: mapInline(t.tokens), href: t.href });
121
- break;
122
- case 'image':
123
- // no image rendering: alt text as a link to the source
124
- nodes.push({
125
- type: 'link',
126
- children: [{ type: 'text', text: t.text || t.href }],
127
- href: t.href
128
- });
129
- break;
130
- case 'br':
131
- pushText(' ');
132
- break;
133
- case 'html':
134
- pushText(t.raw);
135
- break;
136
- default:
137
- if (t.text) pushText(t.text);
138
- break;
139
- }
140
- }
141
- return nodes;
142
- }
143
-
144
- const ENTITIES = { '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&#39;': "'" };
145
-
146
- function unescapeHtml(text) {
147
- return text.replace(/&(?:amp|lt|gt|quot|#39);/g, (m) => ENTITIES[m]);
148
- }
@@ -1,492 +0,0 @@
1
- import { TextLayout } from '../text/layout.js';
2
- import { tokenize } from './highlight.js';
3
- import { parseMarkdown } from './markdown.js';
4
- import { layoutTex } from './tex.js';
5
-
6
- const MATH_LANGS = new Set(['math', 'tex', 'latex', 'katex']);
7
-
8
- const DEFAULT_THEME = {
9
- family: 'sans-serif',
10
- monoFamily: 'monospace',
11
- size: 16,
12
- color: '#222222',
13
- background: 'white',
14
- lineHeight: 1.35,
15
- headingScales: [2, 1.5, 1.25, 1.1, 1, 0.9],
16
- linkColor: '#0b61c9',
17
- codeColor: '#8f2b4f',
18
- codeBg: '#f2f2f2',
19
- quoteColor: '#555555',
20
- quoteBar: '#d8d8d8',
21
- hrColor: '#dddddd',
22
- tableBorder: '#d8d8d8',
23
- tableHeaderBg: '#f7f7f7',
24
- blockSpacing: 0.7, // em (of base size) between blocks
25
- padding: 16,
26
- // fence syntax highlighting (lib/widgets/highlight.js token kinds);
27
- // unlisted kinds (incl. 'plain') fall back to the base text color
28
- codeTheme: {
29
- keyword: '#a626a4',
30
- literal: '#0184bc',
31
- string: '#50a14f',
32
- number: '#986801',
33
- comment: '#a0a1a7',
34
- tag: '#e45649',
35
- attr: '#986801',
36
- function: '#4078f2'
37
- }
38
- };
39
-
40
- /**
41
- * A widget that renders markdown into a window (or any 2d context target).
42
- *
43
- * All layout runs client-side through the text API (`FontManager` /
44
- * `TextLayout`): heading/paragraph/list/quote/code blocks become styled
45
- * spans, get shaped (kerning, fallback, bidi) and wrapped to the container
46
- * width; drawing batches glyphs into minimal CompositeGlyphs requests.
47
- *
48
- * const view = new MarkdownView(wnd, { theme: { size: 15 } });
49
- * view.setMarkdown('# Hello\n\nSome *markdown* here.');
50
- * wnd.map();
51
- *
52
- * Standalone use (no window attach): `view.layout(width)` then
53
- * `view.draw(ctx, x, y)`; `view.contentHeight` after layout. Layout is
54
- * synchronous throughout — nothing here loads in the background.
55
- */
56
- export default class MarkdownView {
57
- constructor(window, opts = {}) {
58
- this.window = window ?? null;
59
- this._standaloneFonts = opts.fonts ?? null; // for headless layout without a window
60
- this.theme = { ...DEFAULT_THEME, ...(opts.theme || {}) };
61
- if (opts.theme?.codeTheme) {
62
- this.theme.codeTheme = { ...DEFAULT_THEME.codeTheme, ...opts.theme.codeTheme };
63
- }
64
- if (opts.padding !== undefined) this.theme.padding = opts.padding;
65
- this._blocks = [];
66
- this._items = []; // placed draw items from the last layout()
67
- this._links = []; // clickable link rects from the last draw()
68
- this._layoutWidth = -1;
69
- this.contentHeight = 0;
70
- /** called with (href, event) when a rendered link is clicked (window
71
- * mode wires mousedown automatically; standalone callers use linkAt) */
72
- this.onLink = opts.onLink ?? null;
73
-
74
- if (this.window) {
75
- this._ctx = this.window.getContext('2d');
76
- this.window.on('expose', () => this.render());
77
- // a move is a ConfigureNotify too; re-laying out a document per step
78
- // of a window drag is exactly what ev.resized is for
79
- this.window.on('resize', (ev) => {
80
- if (ev.resized) this._layoutWidth = -1; // invalidate
81
- });
82
- this.window.on('mousedown', (ev) => {
83
- if (ev.keycode !== 1 || !this.onLink) return; // left button only
84
- const href = this.linkAt(ev.x, ev.y);
85
- if (href) this.onLink(href, ev);
86
- });
87
- }
88
- }
89
-
90
- /**
91
- * The link href under a point, or null. Coordinates are in the same
92
- * device space as the last `draw()` (window mode: window coordinates).
93
- */
94
- linkAt(x, y) {
95
- for (const l of this._links) {
96
- if (x >= l.x && x < l.x + l.w && y >= l.y && y < l.y + l.h) return l.href;
97
- }
98
- return null;
99
- }
100
-
101
- setMarkdown(src) {
102
- this._blocks = parseMarkdown(src);
103
- this._layoutWidth = -1;
104
- if (this.window && this.window._mapped) this.render();
105
- return this;
106
- }
107
-
108
- /** lay content out for a container width; returns total content height */
109
- layout(width) {
110
- const fonts = this._fonts();
111
- this._items = [];
112
- const t = this.theme;
113
- const inner = Math.max(20, width - t.padding * 2);
114
- const y = this._layoutBlocks(fonts, this._blocks, this._blockStyles(), t.padding, 0, inner);
115
- this.contentHeight = y + t.padding * 2;
116
- this._layoutWidth = width;
117
- return this.contentHeight;
118
- }
119
-
120
- _fonts() {
121
- if (this.window) return this.window.app.fonts;
122
- if (!this._standaloneFonts) {
123
- throw new Error('MarkdownView without a window needs { fonts: new FontManager() }');
124
- }
125
- return this._standaloneFonts;
126
- }
127
-
128
- _blockStyles() {
129
- const t = this.theme;
130
- return {
131
- base: { family: t.family, size: t.size, color: t.color },
132
- mono: { family: t.monoFamily, size: t.size * 0.9, color: t.codeColor }
133
- };
134
- }
135
-
136
- /** recursive block layout; returns the y after the last block */
137
- _layoutBlocks(fonts, blocks, styles, x, y, width) {
138
- const t = this.theme;
139
- const spacing = t.blockSpacing * t.size;
140
- let first = true;
141
-
142
- for (const block of blocks) {
143
- if (!first) y += spacing;
144
- first = false;
145
-
146
- switch (block.type) {
147
- case 'heading': {
148
- const scale = t.headingScales[block.level - 1] ?? 1;
149
- const style = {
150
- ...styles.base,
151
- size: Math.round(t.size * scale),
152
- weight: 700
153
- };
154
- y += block.level <= 2 ? spacing * 0.5 : 0;
155
- const layout = new TextLayout(fonts, this._spans(block.children, style), style, {
156
- maxWidth: width,
157
- lineHeight: t.lineHeight
158
- });
159
- this._items.push({ kind: 'text', x, y, layout });
160
- y += layout.height;
161
- break;
162
- }
163
-
164
- case 'paragraph': {
165
- const style = styles.base;
166
- const layout = new TextLayout(fonts, this._spans(block.children, style), style, {
167
- maxWidth: width,
168
- lineHeight: t.lineHeight
169
- });
170
- this._items.push({ kind: 'text', x, y, layout });
171
- y += layout.height;
172
- break;
173
- }
174
-
175
- case 'code': {
176
- // ```math / ```latex fences render as display-mode formulas
177
- if (MATH_LANGS.has((block.lang || '').toLowerCase())) {
178
- let box = null;
179
- try {
180
- // 1.21 mirrors katex.css's font-size on .katex in body text
181
- box = layoutTex(block.text, {
182
- size: t.size * 1.21,
183
- displayMode: true,
184
- color: t.color
185
- });
186
- } catch {
187
- // KaTeX parse error: fall through to the plain code block
188
- }
189
- if (box) {
190
- const bx = x + Math.max(0, (width - box.width) / 2);
191
- this._items.push({ kind: 'tex', x: bx, y, box });
192
- y += box.height;
193
- break;
194
- }
195
- }
196
- const pad = t.size * 0.5;
197
- const style = { ...styles.mono, color: t.color };
198
- const spans = tokenize(block.text, block.lang).map((tok) => ({
199
- text: tok.text,
200
- color: t.codeTheme?.[tok.kind] ?? style.color
201
- }));
202
- const layout = new TextLayout(fonts, spans.length ? spans : '', style, {
203
- maxWidth: width - pad * 2,
204
- lineHeight: 1.25
205
- });
206
- this._items.push({
207
- kind: 'rect',
208
- x,
209
- y,
210
- width,
211
- height: layout.height + pad * 2,
212
- color: t.codeBg
213
- });
214
- this._items.push({ kind: 'text', x: x + pad, y: y + pad, layout });
215
- y += layout.height + pad * 2;
216
- break;
217
- }
218
-
219
- case 'blockquote': {
220
- const indent = t.size;
221
- const startY = y;
222
- const bar = { kind: 'rect', x, y, width: 3, height: 0, color: t.quoteBar };
223
- this._items.push(bar);
224
- const quoted = {
225
- base: { ...styles.base, color: t.quoteColor },
226
- mono: styles.mono
227
- };
228
- y = this._layoutBlocks(fonts, block.blocks, quoted, x + indent, y, width - indent);
229
- bar.height = y - startY;
230
- break;
231
- }
232
-
233
- case 'list': {
234
- const indent = t.size * 1.6;
235
- let n = block.start;
236
- for (let it = 0; it < block.items.length; it++) {
237
- if (it > 0) y += spacing * 0.4;
238
- const marker = block.ordered ? `${n++}.` : '•';
239
- const markerLayout = new TextLayout(fonts, marker, styles.base, {});
240
- this._items.push({ kind: 'text', x, y, layout: markerLayout });
241
- y = this._layoutBlocks(fonts, block.items[it], styles, x + indent, y, width - indent);
242
- }
243
- break;
244
- }
245
-
246
- case 'table': {
247
- y = this._layoutTable(fonts, block, styles, x, y, width);
248
- break;
249
- }
250
-
251
- case 'hr': {
252
- this._items.push({
253
- kind: 'rect',
254
- x,
255
- y: y + spacing * 0.5,
256
- width,
257
- height: 1,
258
- color: t.hrColor
259
- });
260
- y += spacing;
261
- break;
262
- }
263
- }
264
- }
265
- return y;
266
- }
267
-
268
- /**
269
- * Table layout: columns take their natural (unwrapped) width when the
270
- * table fits, otherwise shrink proportionally (with a floor) and cells
271
- * wrap. The grid and header background are emitted as rect items, so the
272
- * two-pass draw() paints them under the cell text.
273
- */
274
- _layoutTable(fonts, block, styles, x, y, width) {
275
- const t = this.theme;
276
- const hpad = t.size * 0.55;
277
- const vpad = t.size * 0.3;
278
- const ncols = block.header.length;
279
- const allRows = [block.header, ...block.rows];
280
-
281
- // natural width per column: widest unwrapped cell
282
- const cellStyle = (isHeader) => (isHeader ? { ...styles.base, weight: 700 } : styles.base);
283
- const cellSpans = allRows.map((row, r) =>
284
- Array.from({ length: ncols }, (_, c) => this._spans(row[c] ?? [], cellStyle(r === 0)))
285
- );
286
- const natural = new Array(ncols).fill(0);
287
- for (const row of cellSpans) {
288
- for (let c = 0; c < ncols; c++) {
289
- if (!row[c].length) continue;
290
- const measure = new TextLayout(fonts, row[c], styles.base, { lineHeight: t.lineHeight });
291
- natural[c] = Math.max(natural[c], measure.width);
292
- }
293
- }
294
-
295
- // Two widths per column from here on: `cols` is the outer width the grid
296
- // is drawn at, `inner` is what a cell is laid out at. `inner` is kept
297
- // alongside rather than derived as `cols[c] - hpad * 2`, and that is not
298
- // tidiness. Adding a padding and subtracting it again is not the identity
299
- // in binary floating point — for a natural width of 48.953125 at size 16
300
- // the round trip returns 48.95312499999999 — and a token handed a maxWidth
301
- // a few ULPs below its own measured width is *wider than its container*,
302
- // so TextLayout force-breaks it. That is a `value` header rendered as
303
- // `valu` over `e`, on some fonts and not others, depending on nothing more
304
- // than whether the width happens to survive the round trip.
305
- const cols = natural.map((w) => w + hpad * 2);
306
- const inner = natural.slice();
307
- const total = cols.reduce((a, b) => a + b, 0);
308
- if (total > width) {
309
- // Shrink towards each column's *min-content* width — the widest word it
310
- // holds that cannot be broken — and never past it. The floor used to be
311
- // a flat 2.5em guess with no relation to the content, so a column whose
312
- // longest word was wider than that got a maxWidth below the word and the
313
- // word came apart.
314
- //
315
- // Min-content is the widest whitespace-delimited token, each measured
316
- // *unconstrained*. Laying the whole cell out at a tiny maxWidth looks
317
- // like the obvious probe and is not one: a token wider than the
318
- // container is force-broken rather than left to overflow whenever a
319
- // single cluster fits, so the probe reports a fragment's width. It is
320
- // not even monotonic in maxWidth — see the test that pins that. An
321
- // unconstrained token cannot be broken at all.
322
- //
323
- // A token split across spans — `**val**ue` — is measured as its halves
324
- // and so under-measured. Rare, and still far above the flat guess this
325
- // replaced. Only measured here: a table that fits needs no minimums.
326
- const content = new Array(ncols).fill(0);
327
- for (const row of cellSpans) {
328
- for (let c = 0; c < ncols; c++) {
329
- for (const span of row[c]) {
330
- for (const token of String(span.text ?? '').split(/\s+/)) {
331
- if (!token) continue;
332
- const measure = new TextLayout(fonts, [{ ...span, text: token }], styles.base, {
333
- lineHeight: t.lineHeight
334
- });
335
- content[c] = Math.max(content[c], measure.width);
336
- }
337
- }
338
- }
339
- }
340
- const mins = content.map((w) => w + hpad * 2);
341
- // Space above the minimums is shared in proportion to how much each
342
- // column asked for, so a wide column gives up more than a narrow one.
343
- // When the minimums themselves do not fit, the table overflows rather
344
- // than breaking words — which is what a browser does with the same
345
- // content, and is the lesser of the two evils.
346
- const slack = width - mins.reduce((a, b) => a + b, 0);
347
- const want = cols.reduce((a, w, c) => a + Math.max(0, w - mins[c]), 0);
348
- for (let c = 0; c < ncols; c++) {
349
- cols[c] =
350
- slack <= 0 || want <= 0
351
- ? mins[c]
352
- : mins[c] + (Math.max(0, cols[c] - mins[c]) / want) * slack;
353
- // Never below min-content, whatever the arithmetic above produced.
354
- inner[c] = Math.max(content[c], cols[c] - hpad * 2);
355
- }
356
- }
357
- const tableW = cols.reduce((a, b) => a + b, 0);
358
-
359
- const topY = y;
360
- const rowLines = [y];
361
- for (let r = 0; r < cellSpans.length; r++) {
362
- const layouts = cellSpans[r].map((spans, c) =>
363
- new TextLayout(fonts, spans.length ? spans : '', styles.base, {
364
- maxWidth: inner[c],
365
- align: block.align[c] ?? 'left',
366
- lineHeight: t.lineHeight
367
- })
368
- );
369
- const rowH = Math.max(...layouts.map((l) => l.height), t.size * t.lineHeight) + vpad * 2;
370
- if (r === 0) {
371
- this._items.push({ kind: 'rect', x, y, width: tableW, height: rowH, color: t.tableHeaderBg });
372
- }
373
- let cx = x;
374
- for (let c = 0; c < ncols; c++) {
375
- this._items.push({ kind: 'text', x: cx + hpad, y: y + vpad, layout: layouts[c] });
376
- cx += cols[c];
377
- }
378
- y += rowH;
379
- rowLines.push(y);
380
- }
381
-
382
- // grid: horizontal rule per row boundary, vertical per column boundary
383
- for (const ly of rowLines) {
384
- this._items.push({ kind: 'rect', x, y: ly, width: tableW, height: 1, color: t.tableBorder });
385
- }
386
- let cx = x;
387
- for (let c = 0; c <= ncols; c++) {
388
- this._items.push({ kind: 'rect', x: cx, y: topY, width: 1, height: y - topY + 1, color: t.tableBorder });
389
- if (c < ncols) cx += cols[c];
390
- }
391
- return y + 1;
392
- }
393
-
394
- /** inline AST -> styled spans for TextLayout */
395
- _spans(children, base, state = {}, out = []) {
396
- const t = this.theme;
397
- for (const node of children) {
398
- switch (node.type) {
399
- case 'text':
400
- out.push({
401
- text: node.text,
402
- family: state.code ? t.monoFamily : base.family,
403
- size: state.code ? Math.round(base.size * 0.9) : base.size,
404
- weight: state.bold ? 700 : base.weight,
405
- style: state.italic ? 'italic' : base.style,
406
- color: state.code ? t.codeColor : state.link ? t.linkColor : base.color,
407
- _deco: state.code ? { bg: t.codeBg } : state.link ? { underline: t.linkColor } : null,
408
- _href: state.link ? state.href : null
409
- });
410
- break;
411
- case 'code':
412
- this._spans([{ type: 'text', text: node.text }], base, { ...state, code: true }, out);
413
- break;
414
- case 'strong':
415
- this._spans(node.children, base, { ...state, bold: true }, out);
416
- break;
417
- case 'em':
418
- this._spans(node.children, base, { ...state, italic: true }, out);
419
- break;
420
- case 'link':
421
- this._spans(node.children, base, { ...state, link: true, href: node.href }, out);
422
- break;
423
- }
424
- }
425
- return out;
426
- }
427
-
428
- /** draw the last layout()'s items onto a 2d context */
429
- draw(ctx, ox = 0, oy = 0) {
430
- this._links = [];
431
- for (const item of this._items) {
432
- if (item.kind === 'rect') {
433
- ctx.fillStyle = item.color;
434
- ctx.fillRect(Math.round(ox + item.x), Math.round(oy + item.y), Math.ceil(item.width), Math.ceil(item.height));
435
- }
436
- }
437
- for (const item of this._items) {
438
- if (item.kind === 'tex') {
439
- item.box.draw(ctx, ox + item.x, oy + item.y);
440
- continue;
441
- }
442
- if (item.kind !== 'text') continue;
443
- const x = ox + item.x;
444
- const y = oy + item.y;
445
- // span decorations (inline-code background, link underline) first
446
- for (const line of item.layout.lines) {
447
- for (const r of line.runs) {
448
- if (r.span._href) {
449
- // clickable region in the device coordinates of this draw
450
- const m = r.run.font.metrics(r.run.size);
451
- this._links.push({
452
- x: x + line.x + r.x,
453
- y: y + line.baseline - m.ascent,
454
- w: r.width,
455
- h: m.ascent + m.descent,
456
- href: r.span._href
457
- });
458
- }
459
- const deco = r.span._deco;
460
- if (!deco) continue;
461
- if (deco.bg) {
462
- const m = r.run.font.metrics(r.run.size);
463
- ctx.fillStyle = deco.bg;
464
- ctx.fillRect(
465
- Math.round(x + line.x + r.x - 2),
466
- Math.round(y + line.baseline - m.ascent),
467
- Math.ceil(r.width + 4),
468
- Math.ceil(m.ascent + m.descent)
469
- );
470
- }
471
- if (deco.underline) {
472
- ctx.fillStyle = deco.underline;
473
- ctx.fillRect(Math.round(x + line.x + r.x), Math.round(y + line.baseline + 2), Math.ceil(r.width), 1);
474
- }
475
- }
476
- }
477
- item.layout.draw(ctx, x, y);
478
- }
479
- return this;
480
- }
481
-
482
- /** full window render: clear background, (re)layout on size change, draw */
483
- render() {
484
- if (!this.window) throw new Error('MarkdownView.render() needs a window');
485
- const ctx = this._ctx;
486
- if (this._layoutWidth !== this.window.width) this.layout(this.window.width);
487
- ctx.fillStyle = this.theme.background;
488
- ctx.fillRect(0, 0, this.window.width, this.window.height);
489
- this.draw(ctx, 0, this.theme.padding);
490
- return this;
491
- }
492
- }