@simple-mark/canvas 0.3.3 → 0.3.5

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.
Files changed (3) hide show
  1. package/index.d.ts +19 -2
  2. package/index.js +79 -17
  3. package/package.json +3 -3
package/index.d.ts CHANGED
@@ -46,23 +46,40 @@ interface TextRun {
46
46
  lineFont: string;
47
47
  fontSize: number;
48
48
  color: string;
49
- /** Behind this text only (inline code). */
49
+ /** Behind this text only (inline code); neighbouring runs with the same one share a box. */
50
50
  background?: string;
51
+ /** Corner radius of that box. */
52
+ radius?: number;
51
53
  strike?: boolean;
52
54
  underline?: boolean;
53
55
  /** Draw character by character on a grid of this column width (tables), wide ones in two columns. */
54
56
  column?: number;
55
57
  }
56
- /** A line's full-width background or bar (code blocks, table rows, quotes). */
58
+ /** A line's full-width background, bar or lines (code blocks, table rows, quotes, headings, rules). */
57
59
  interface LineDecoration {
58
60
  top: number;
59
61
  height: number;
60
62
  background?: string;
63
+ /** Corner radii of the background: top (first line of a code block) and bottom (last line). */
64
+ radius?: {
65
+ top: number;
66
+ bottom: number;
67
+ };
61
68
  /** A bar along the left edge: its color and width. */
62
69
  bar?: {
63
70
  color: string;
64
71
  width: number;
65
72
  };
73
+ /** A line along the bottom edge (under h1, h2). */
74
+ border?: {
75
+ color: string;
76
+ width: number;
77
+ };
78
+ /** A line across the middle (horizontal rules). */
79
+ rule?: {
80
+ color: string;
81
+ width: number;
82
+ };
66
83
  }
67
84
  interface ImageBox {
68
85
  kind: 'image';
package/index.js CHANGED
@@ -1,13 +1,11 @@
1
- import { charWidth, defaultTheme, markdownParser, markdownStyles, previewBlocks, resolveLineLook, resolveLook } from "@simple-mark/model";
1
+ import { STROKES, charWidth, defaultTheme, markdownParser, markdownStyles, previewBlocks, resolveLineLook, resolveLook } from "@simple-mark/model";
2
2
  //#region packages/canvas/src/layout.ts
3
3
  /** Editor sizes in em, so a canvas text at any font size looks like the editor at 15px. */
4
4
  const EM = {
5
5
  previewTop: 4 / 15,
6
6
  previewBottom: 8 / 15,
7
7
  imageMaxHeight: 320 / 15,
8
- imageGap: 8 / 15,
9
- quoteBar: 3 / 15,
10
- quoteIndent: 12 / 15
8
+ imageGap: 8 / 15
11
9
  };
12
10
  const ERROR_COLOR = "#cf222e";
13
11
  /** Characters a line may break between, like CJK in the browser (no spaces needed). */
@@ -54,11 +52,19 @@ function layoutMarkdown(source, options) {
54
52
  }
55
53
  return lo;
56
54
  };
57
- const lineBlocks = lineStarts.map(() => /* @__PURE__ */ new Set());
55
+ const lineBlocks = lineStarts.map(() => ({
56
+ styles: /* @__PURE__ */ new Set(),
57
+ first: /* @__PURE__ */ new Set(),
58
+ last: /* @__PURE__ */ new Set()
59
+ }));
58
60
  for (const block of blocks) {
59
61
  let end = block.to;
60
62
  if (end > block.from && isLineStart.has(end)) end--;
61
- for (let n = lineOf(block.from); n <= lineOf(end); n++) lineBlocks[n].add(block.style);
63
+ const first = lineOf(block.from);
64
+ const last = lineOf(end);
65
+ for (let n = first; n <= last; n++) lineBlocks[n].styles.add(block.style);
66
+ lineBlocks[first].first.add(block.style);
67
+ lineBlocks[last].last.add(block.style);
62
68
  }
63
69
  const previews = /* @__PURE__ */ new Map();
64
70
  for (const preview of previewBlocks(tree, text)) {
@@ -79,12 +85,12 @@ function layoutMarkdown(source, options) {
79
85
  for (let n = 0; n < lineStarts.length; n++) {
80
86
  const from = lineStarts[n];
81
87
  const to = lineEndOf(n);
82
- const styles = [...lineBlocks[n]];
83
- const lineLook = resolveLineLook(styles, theme);
88
+ const styles = [...lineBlocks[n].styles];
89
+ const lineLook = resolveLineLook(styles, theme, lineBlocks[n]);
84
90
  const scale = lineLook.fontScale ?? 1;
85
91
  const lineHeight = style.lineHeight * style.fontSize * scale;
86
92
  const textTop = (lineHeight - style.fontSize * scale) / 2;
87
- const indent = lineLook.borderLeft ? EM.quoteIndent * style.fontSize : 0;
93
+ const indent = (lineLook.indent ?? 0) * style.fontSize;
88
94
  const lineFont = fontOf(resolveLook(styles, [], theme), style, theme, scale);
89
95
  const table = styles.includes("table");
90
96
  const available = wrapWidth - indent;
@@ -158,7 +164,7 @@ function layoutMarkdown(source, options) {
158
164
  visual.at(-1).push(piece);
159
165
  x += piece.width;
160
166
  }
161
- for (const row of visual) {
167
+ for (const [r, row] of visual.entries()) {
162
168
  const first = runs.length;
163
169
  let rx = indent;
164
170
  for (const piece of row) {
@@ -181,6 +187,7 @@ function layoutMarkdown(source, options) {
181
187
  fontSize: style.fontSize * scale,
182
188
  color: look.color ?? style.color,
183
189
  ...look.background ? { background: look.background } : {},
190
+ ...look.background && look.radius ? { radius: look.radius * style.fontSize * scale } : {},
184
191
  ...look.strike ? { strike: true } : {},
185
192
  ...look.underline ? { underline: true } : {},
186
193
  ...table ? { column: measure("0", font) } : {}
@@ -195,13 +202,28 @@ function layoutMarkdown(source, options) {
195
202
  rx
196
203
  ]);
197
204
  maxWidth = Math.max(maxWidth, rx);
198
- if (lineLook.lineBackground || lineLook.borderLeft) lines.push({
205
+ const radiusTop = r === 0 ? (lineLook.radiusTop ?? 0) * style.fontSize : 0;
206
+ const radiusBottom = r === visual.length - 1 ? (lineLook.radiusBottom ?? 0) * style.fontSize : 0;
207
+ const border = r === visual.length - 1 ? lineLook.borderBottom : void 0;
208
+ if (lineLook.lineBackground || lineLook.borderLeft || border || lineLook.rule) lines.push({
199
209
  top: y,
200
210
  height: lineHeight,
201
211
  ...lineLook.lineBackground ? { background: lineLook.lineBackground } : {},
212
+ ...lineLook.lineBackground && (radiusTop || radiusBottom) ? { radius: {
213
+ top: radiusTop,
214
+ bottom: radiusBottom
215
+ } } : {},
202
216
  ...lineLook.borderLeft ? { bar: {
203
217
  color: lineLook.borderLeft,
204
- width: EM.quoteBar * style.fontSize
218
+ width: STROKES.bar * style.fontSize
219
+ } } : {},
220
+ ...border ? { border: {
221
+ color: border,
222
+ width: STROKES.border * style.fontSize
223
+ } } : {},
224
+ ...lineLook.rule ? { rule: {
225
+ color: lineLook.rule,
226
+ width: STROKES.rule * style.fontSize
205
227
  } } : {}
206
228
  });
207
229
  y += lineHeight;
@@ -381,27 +403,67 @@ function baseline(ctx, run) {
381
403
  alphabetic: true
382
404
  };
383
405
  }
406
+ /** A filled rectangle with rounded corners (top-left, top-right, bottom-right, bottom-left). */
407
+ function fillBox(ctx, x, y, width, height, radius) {
408
+ const radii = typeof radius === "number" ? [
409
+ radius,
410
+ radius,
411
+ radius,
412
+ radius
413
+ ] : radius;
414
+ if (!radii.some((r) => r > 0) || !ctx.roundRect) {
415
+ ctx.fillRect(x, y, width, height);
416
+ return;
417
+ }
418
+ ctx.beginPath();
419
+ ctx.roundRect(x, y, width, height, radii.map((r) => Math.min(r, width / 2, height / 2)));
420
+ ctx.fill();
421
+ }
384
422
  function paintMarkdown(ctx, layout, sources = {}, color = "#1f2328") {
385
423
  ctx.save();
386
424
  ctx.textAlign = "left";
387
425
  for (const line of layout.lines) {
388
426
  if (line.background) {
389
427
  ctx.fillStyle = line.background;
390
- ctx.fillRect(0, line.top, layout.width, line.height);
428
+ fillBox(ctx, 0, line.top, layout.width, line.height, line.radius ? [
429
+ line.radius.top,
430
+ line.radius.top,
431
+ line.radius.bottom,
432
+ line.radius.bottom
433
+ ] : 0);
434
+ }
435
+ if (line.rule) {
436
+ ctx.fillStyle = line.rule.color;
437
+ ctx.fillRect(0, line.top + (line.height - line.rule.width) / 2, layout.width, line.rule.width);
391
438
  }
392
439
  if (line.bar) {
393
440
  ctx.fillStyle = line.bar.color;
394
441
  ctx.fillRect(0, line.top, line.bar.width, line.height);
395
442
  }
443
+ if (line.border) {
444
+ ctx.fillStyle = line.border.color;
445
+ ctx.fillRect(0, line.top + line.height - line.border.width, layout.width, line.border.width);
446
+ }
447
+ }
448
+ for (let i = 0; i < layout.runs.length;) {
449
+ const run = layout.runs[i];
450
+ let end = run.x + run.width;
451
+ let j = i + 1;
452
+ if (run.background) {
453
+ for (; j < layout.runs.length; j++) {
454
+ const next = layout.runs[j];
455
+ if (next.background !== run.background || next.top !== run.top || Math.abs(next.x - end) > .5) break;
456
+ end = next.x + next.width;
457
+ }
458
+ ctx.fillStyle = run.background;
459
+ fillBox(ctx, run.x, run.textTop - run.fontSize * .1, end - run.x, run.fontSize * 1.2, run.radius ?? 0);
460
+ }
461
+ i = j;
396
462
  }
397
463
  for (const run of layout.runs) {
398
464
  const { y, alphabetic } = baseline(ctx, run);
399
465
  ctx.font = run.font;
400
466
  ctx.textBaseline = alphabetic ? "alphabetic" : "top";
401
- if (run.background) {
402
- ctx.fillStyle = run.background;
403
- ctx.fillRect(run.x, run.textTop - run.fontSize * .1, run.width, run.fontSize * 1.2);
404
- }
405
467
  ctx.fillStyle = run.color;
406
468
  if (run.column) {
407
469
  let x = run.x;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simple-mark/canvas",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Draws Markdown on a <canvas> without hiding its syntax: layout (no DOM) and Canvas 2D painting.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  }
24
24
  },
25
25
  "dependencies": {
26
- "@simple-mark/math": "^0.3.3",
27
- "@simple-mark/model": "^0.3.3"
26
+ "@simple-mark/math": "^0.3.5",
27
+ "@simple-mark/model": "^0.3.5"
28
28
  }
29
29
  }