@vectojs/markdown 0.1.1 → 0.1.2

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.
@@ -37,6 +37,8 @@ export interface MarkdownTheme {
37
37
  export declare class CodeBlock extends UIComponent {
38
38
  private lines;
39
39
  private grid;
40
+ /** Raw (unhighlighted) lines of the last build, for prefix reuse in buildLines. */
41
+ private rawLines;
40
42
  private cellWidth;
41
43
  private source;
42
44
  private lang;
@@ -51,6 +53,18 @@ export declare class CodeBlock extends UIComponent {
51
53
  /** Enable or disable browser-native selection for this code block. */
52
54
  setSelectable(selectable: boolean): this;
53
55
  getContentProjection(): ContentProjection | null;
56
+ /**
57
+ * Re-highlight the code, reusing the highlight of any unchanged line prefix.
58
+ *
59
+ * Streaming appends to the END of a block, so all but the last line or two are
60
+ * byte-identical to the previous call — yet this used to re-highlight every
61
+ * line on every chunk, making a streamed block O(N) per append and O(N^2)
62
+ * overall. Reusing the stable prefix makes an append proportional to what
63
+ * actually changed.
64
+ *
65
+ * The last previously-seen line is deliberately NOT reused: a chunk usually
66
+ * lands mid-line, so that line's text (and therefore its tokenization) changes.
67
+ */
54
68
  private buildLines;
55
69
  private ensureGrid;
56
70
  /** Code blocks are decorative — not interactive. */
package/dist/index.js CHANGED
@@ -438,6 +438,8 @@ function highlightLine(line, lang, theme) {
438
438
  var CodeBlock = class extends import_ui.UIComponent {
439
439
  lines;
440
440
  grid = null;
441
+ /** Raw (unhighlighted) lines of the last build, for prefix reuse in buildLines. */
442
+ rawLines = null;
441
443
  cellWidth = 0;
442
444
  source;
443
445
  lang;
@@ -497,9 +499,36 @@ var CodeBlock = class extends import_ui.UIComponent {
497
499
  grid
498
500
  };
499
501
  }
502
+ /**
503
+ * Re-highlight the code, reusing the highlight of any unchanged line prefix.
504
+ *
505
+ * Streaming appends to the END of a block, so all but the last line or two are
506
+ * byte-identical to the previous call — yet this used to re-highlight every
507
+ * line on every chunk, making a streamed block O(N) per append and O(N^2)
508
+ * overall. Reusing the stable prefix makes an append proportional to what
509
+ * actually changed.
510
+ *
511
+ * The last previously-seen line is deliberately NOT reused: a chunk usually
512
+ * lands mid-line, so that line's text (and therefore its tokenization) changes.
513
+ */
500
514
  buildLines(code) {
501
515
  const rawLines = code.split(/\r\n|\r|\n/);
502
- this.lines = rawLines.map((l) => highlightLine(l, this.lang, this.theme));
516
+ const previous = this.rawLines;
517
+ let reusable = 0;
518
+ if (previous && this.lines.length === previous.length) {
519
+ const limit = Math.min(previous.length - 1, rawLines.length);
520
+ while (reusable < limit && previous[reusable] === rawLines[reusable]) reusable++;
521
+ }
522
+ if (reusable > 0) {
523
+ const next = this.lines.slice(0, reusable);
524
+ for (let i = reusable; i < rawLines.length; i++) {
525
+ next.push(highlightLine(rawLines[i], this.lang, this.theme));
526
+ }
527
+ this.lines = next;
528
+ } else {
529
+ this.lines = rawLines.map((l) => highlightLine(l, this.lang, this.theme));
530
+ }
531
+ this.rawLines = rawLines;
503
532
  this.grid = null;
504
533
  this.height = this.pad * 2 + rawLines.length * this.lineH;
505
534
  }
@@ -831,7 +860,7 @@ var Markdown = class extends import_ui.UIComponent {
831
860
  this.pendingWorkerIds.delete(id);
832
861
  this.appendInFlight = false;
833
862
  const newTokens = [...oldTokensSnapshot.slice(0, matchLen), ...tail];
834
- this.updateTokens(newTokens);
863
+ this.updateTokens(newTokens, matchLen);
835
864
  if (this.appendPending) {
836
865
  this.appendPending = false;
837
866
  this.dispatchAppend();
@@ -855,21 +884,35 @@ var Markdown = class extends import_ui.UIComponent {
855
884
  ...sendRaws ? { oldRaws: oldTokensSnapshot.map((t) => t.raw) } : {}
856
885
  });
857
886
  }
858
- updateTokens(newTokens) {
887
+ updateTokens(newTokens, knownMatchLen) {
859
888
  const oldTokens = this.tokens;
860
889
  const oldChildren = [...this.content.children];
861
- let matchLen = 0;
890
+ let matchLen;
862
891
  const minLen = Math.min(oldTokens.length, newTokens.length);
863
- for (let i = 0; i < minLen; i++) {
864
- if (oldTokens[i].raw === newTokens[i].raw) {
865
- matchLen++;
866
- } else {
867
- break;
892
+ if (knownMatchLen !== void 0 && knownMatchLen >= 0 && knownMatchLen <= minLen) {
893
+ matchLen = knownMatchLen;
894
+ } else {
895
+ matchLen = 0;
896
+ for (let i = 0; i < minLen; i++) {
897
+ if (oldTokens[i].raw === newTokens[i].raw) {
898
+ matchLen++;
899
+ } else {
900
+ break;
901
+ }
868
902
  }
869
903
  }
870
904
  const oldTokenToChild = this.tokenChildPrefix;
871
905
  const rawMatchLen = matchLen;
872
- if (matchLen === oldTokens.length - 1 && matchLen < newTokens.length && oldTokens[matchLen]?.type === newTokens[matchLen]?.type && newTokens[matchLen]?.type === "paragraph") {
906
+ const lastTokenSameType = matchLen === oldTokens.length - 1 && matchLen < newTokens.length && oldTokens[matchLen]?.type === newTokens[matchLen]?.type;
907
+ if (lastTokenSameType && newTokens[matchLen]?.type === "code") {
908
+ const existingEntity = oldChildren[oldTokenToChild[matchLen]];
909
+ const codeToken = newTokens[matchLen];
910
+ if (existingEntity instanceof CodeBlock) {
911
+ existingEntity.setCode(codeToken.text, codeToken.lang ?? void 0);
912
+ matchLen++;
913
+ this.content.resizeLastChild(existingEntity);
914
+ }
915
+ } else if (lastTokenSameType && newTokens[matchLen]?.type === "paragraph") {
873
916
  const entityIdx = oldTokenToChild[matchLen];
874
917
  const existingEntity = oldChildren[entityIdx];
875
918
  if (existingEntity && "setSpans" in existingEntity) {
package/dist/index.mjs CHANGED
@@ -416,6 +416,8 @@ function highlightLine(line, lang, theme) {
416
416
  var CodeBlock = class extends UIComponent {
417
417
  lines;
418
418
  grid = null;
419
+ /** Raw (unhighlighted) lines of the last build, for prefix reuse in buildLines. */
420
+ rawLines = null;
419
421
  cellWidth = 0;
420
422
  source;
421
423
  lang;
@@ -475,9 +477,36 @@ var CodeBlock = class extends UIComponent {
475
477
  grid
476
478
  };
477
479
  }
480
+ /**
481
+ * Re-highlight the code, reusing the highlight of any unchanged line prefix.
482
+ *
483
+ * Streaming appends to the END of a block, so all but the last line or two are
484
+ * byte-identical to the previous call — yet this used to re-highlight every
485
+ * line on every chunk, making a streamed block O(N) per append and O(N^2)
486
+ * overall. Reusing the stable prefix makes an append proportional to what
487
+ * actually changed.
488
+ *
489
+ * The last previously-seen line is deliberately NOT reused: a chunk usually
490
+ * lands mid-line, so that line's text (and therefore its tokenization) changes.
491
+ */
478
492
  buildLines(code) {
479
493
  const rawLines = code.split(/\r\n|\r|\n/);
480
- this.lines = rawLines.map((l) => highlightLine(l, this.lang, this.theme));
494
+ const previous = this.rawLines;
495
+ let reusable = 0;
496
+ if (previous && this.lines.length === previous.length) {
497
+ const limit = Math.min(previous.length - 1, rawLines.length);
498
+ while (reusable < limit && previous[reusable] === rawLines[reusable]) reusable++;
499
+ }
500
+ if (reusable > 0) {
501
+ const next = this.lines.slice(0, reusable);
502
+ for (let i = reusable; i < rawLines.length; i++) {
503
+ next.push(highlightLine(rawLines[i], this.lang, this.theme));
504
+ }
505
+ this.lines = next;
506
+ } else {
507
+ this.lines = rawLines.map((l) => highlightLine(l, this.lang, this.theme));
508
+ }
509
+ this.rawLines = rawLines;
481
510
  this.grid = null;
482
511
  this.height = this.pad * 2 + rawLines.length * this.lineH;
483
512
  }
@@ -809,7 +838,7 @@ var Markdown = class extends UIComponent {
809
838
  this.pendingWorkerIds.delete(id);
810
839
  this.appendInFlight = false;
811
840
  const newTokens = [...oldTokensSnapshot.slice(0, matchLen), ...tail];
812
- this.updateTokens(newTokens);
841
+ this.updateTokens(newTokens, matchLen);
813
842
  if (this.appendPending) {
814
843
  this.appendPending = false;
815
844
  this.dispatchAppend();
@@ -833,21 +862,35 @@ var Markdown = class extends UIComponent {
833
862
  ...sendRaws ? { oldRaws: oldTokensSnapshot.map((t) => t.raw) } : {}
834
863
  });
835
864
  }
836
- updateTokens(newTokens) {
865
+ updateTokens(newTokens, knownMatchLen) {
837
866
  const oldTokens = this.tokens;
838
867
  const oldChildren = [...this.content.children];
839
- let matchLen = 0;
868
+ let matchLen;
840
869
  const minLen = Math.min(oldTokens.length, newTokens.length);
841
- for (let i = 0; i < minLen; i++) {
842
- if (oldTokens[i].raw === newTokens[i].raw) {
843
- matchLen++;
844
- } else {
845
- break;
870
+ if (knownMatchLen !== void 0 && knownMatchLen >= 0 && knownMatchLen <= minLen) {
871
+ matchLen = knownMatchLen;
872
+ } else {
873
+ matchLen = 0;
874
+ for (let i = 0; i < minLen; i++) {
875
+ if (oldTokens[i].raw === newTokens[i].raw) {
876
+ matchLen++;
877
+ } else {
878
+ break;
879
+ }
846
880
  }
847
881
  }
848
882
  const oldTokenToChild = this.tokenChildPrefix;
849
883
  const rawMatchLen = matchLen;
850
- if (matchLen === oldTokens.length - 1 && matchLen < newTokens.length && oldTokens[matchLen]?.type === newTokens[matchLen]?.type && newTokens[matchLen]?.type === "paragraph") {
884
+ const lastTokenSameType = matchLen === oldTokens.length - 1 && matchLen < newTokens.length && oldTokens[matchLen]?.type === newTokens[matchLen]?.type;
885
+ if (lastTokenSameType && newTokens[matchLen]?.type === "code") {
886
+ const existingEntity = oldChildren[oldTokenToChild[matchLen]];
887
+ const codeToken = newTokens[matchLen];
888
+ if (existingEntity instanceof CodeBlock) {
889
+ existingEntity.setCode(codeToken.text, codeToken.lang ?? void 0);
890
+ matchLen++;
891
+ this.content.resizeLastChild(existingEntity);
892
+ }
893
+ } else if (lastTokenSameType && newTokens[matchLen]?.type === "paragraph") {
851
894
  const entityIdx = oldTokenToChild[matchLen];
852
895
  const existingEntity = oldChildren[entityIdx];
853
896
  if (existingEntity && "setSpans" in existingEntity) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/markdown",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },