@gilav21/shadcn-angular-rte 0.1.2 → 0.2.1

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 (24) hide show
  1. package/README.md +43 -27
  2. package/fesm2022/{gilav21-shadcn-angular-rte-docx-parser-B8BeJWtY.mjs → gilav21-shadcn-angular-rte-docx-parser-DiBPweI4.mjs} +63 -38
  3. package/fesm2022/gilav21-shadcn-angular-rte-docx-parser-DiBPweI4.mjs.map +1 -0
  4. package/fesm2022/{gilav21-shadcn-angular-rte-docx-to-editor-html-Cl9eIhkC.mjs → gilav21-shadcn-angular-rte-docx-to-editor-html-BreagmS_.mjs} +43 -33
  5. package/fesm2022/gilav21-shadcn-angular-rte-docx-to-editor-html-BreagmS_.mjs.map +1 -0
  6. package/fesm2022/{gilav21-shadcn-angular-rte-gilav21-shadcn-angular-rte-CfjU_y-y.mjs → gilav21-shadcn-angular-rte-gilav21-shadcn-angular-rte-BnEAQOuU.mjs} +3561 -1901
  7. package/fesm2022/gilav21-shadcn-angular-rte-gilav21-shadcn-angular-rte-BnEAQOuU.mjs.map +1 -0
  8. package/fesm2022/gilav21-shadcn-angular-rte-markup-safety-qJk8EoPk.mjs +69 -0
  9. package/fesm2022/gilav21-shadcn-angular-rte-markup-safety-qJk8EoPk.mjs.map +1 -0
  10. package/fesm2022/{gilav21-shadcn-angular-rte-pdf-readable-DmE-CIxv.mjs → gilav21-shadcn-angular-rte-pdf-readable-JvCpuNQH.mjs} +445 -124
  11. package/fesm2022/gilav21-shadcn-angular-rte-pdf-readable-JvCpuNQH.mjs.map +1 -0
  12. package/fesm2022/gilav21-shadcn-angular-rte.mjs +1 -1
  13. package/package.json +6 -2
  14. package/schematics/collection.json +10 -0
  15. package/schematics/ng-add/index.cjs +169 -0
  16. package/schematics/ng-add/schema.json +13 -0
  17. package/styles.css +2 -0
  18. package/types/gilav21-shadcn-angular-rte.d.ts +365 -114
  19. package/types/gilav21-shadcn-angular-rte.d.ts.map +1 -1
  20. package/fesm2022/gilav21-shadcn-angular-rte-docx-parser-B8BeJWtY.mjs.map +0 -1
  21. package/fesm2022/gilav21-shadcn-angular-rte-docx-to-editor-html-Cl9eIhkC.mjs.map +0 -1
  22. package/fesm2022/gilav21-shadcn-angular-rte-gilav21-shadcn-angular-rte-CfjU_y-y.mjs.map +0 -1
  23. package/fesm2022/gilav21-shadcn-angular-rte-pdf-readable-DmE-CIxv.mjs.map +0 -1
  24. package/theme.css +0 -159
@@ -1,4 +1,79 @@
1
1
  import { B as ByteSink, z as zlibInflate } from './gilav21-shadcn-angular-rte-inflate-laXxypva.mjs';
2
+ import { a as cssFontFamily, e as escapeAttribute, s as safeLinkHref } from './gilav21-shadcn-angular-rte-markup-safety-qJk8EoPk.mjs';
3
+
4
+ const DEFAULT_PARSER_LIMITS = Object.freeze({
5
+ maxImagePixels: 50_000_000,
6
+ maxDecodedBytesTotal: 1024 * 1024 * 1024,
7
+ maxNestingDepth: 32,
8
+ maxFormDrawsPerPage: 10_000,
9
+ maxPages: 10_000,
10
+ maxOle2StreamBytes: 256 * 1024 * 1024,
11
+ });
12
+ /**
13
+ * Marks `node` visited. Returns false when it already was, so a walk over a
14
+ * shared or cyclic reference graph visits each node once.
15
+ */
16
+ function visitOnce(seen, node) {
17
+ if (seen.has(node))
18
+ return false;
19
+ seen.add(node);
20
+ return true;
21
+ }
22
+ /**
23
+ * Guards recursive drawing of shared content (form XObjects): a node is
24
+ * skipped when it is already on the current draw path (a cycle), when the path
25
+ * is at the depth cap, or when the draw budget is spent. A node that is merely
26
+ * shared — drawn again from a different path — is drawn every time, because the
27
+ * path is a recursion stack, not an "ever drawn" set.
28
+ */
29
+ class DrawPath {
30
+ constructor(maxDepth, maxDraws) {
31
+ this.maxDepth = maxDepth;
32
+ this.maxDraws = maxDraws;
33
+ this.path = new Set();
34
+ this.draws = 0;
35
+ }
36
+ /** Runs `draw` inside `node` unless a guard trips; returns whether it ran. */
37
+ enter(node, draw) {
38
+ if (this.path.has(node) || this.path.size >= this.maxDepth || this.draws >= this.maxDraws) {
39
+ return false;
40
+ }
41
+ this.draws++;
42
+ this.path.add(node);
43
+ try {
44
+ draw();
45
+ }
46
+ finally {
47
+ this.path.delete(node);
48
+ }
49
+ return true;
50
+ }
51
+ }
52
+ /** A byte budget shared by every stream decode in one parse. */
53
+ class DecodeBudget {
54
+ constructor(total) {
55
+ this.remaining = total;
56
+ }
57
+ /** Charges `bytes`; returns false (charging nothing) when they do not fit. */
58
+ take(bytes) {
59
+ if (bytes > this.remaining)
60
+ return false;
61
+ this.remaining -= bytes;
62
+ return true;
63
+ }
64
+ /** True once nothing is left, so a caller can refuse work before doing it. */
65
+ get spent() {
66
+ return this.remaining <= 0;
67
+ }
68
+ /**
69
+ * Gives up what is left. A charge that did not fit means the work it paid
70
+ * for was already done; spending the rest stops every later decode of the
71
+ * same size from doing that work again just to be refused.
72
+ */
73
+ spendRemainder() {
74
+ this.remaining = 0;
75
+ }
76
+ }
2
77
 
3
78
  /**
4
79
  * Ceiling for a single decoded PDF stream. A stream's filter chain can expand
@@ -369,8 +444,12 @@ function decodeCCITTFax(data, parms, reader) {
369
444
  const rows = reader.getNumber(parms['Rows']) || 0;
370
445
  const encodedByteAlign = !!parms['EncodedByteAlign'];
371
446
  const blackIs1 = !!parms['BlackIs1'];
447
+ // Columns and Rows are the file's claim, not the image's size: a row that
448
+ // alone exceeds the pixel cap is refused, and rows stop where the cap does.
449
+ if (columns <= 0 || columns > reader.limits.maxImagePixels)
450
+ return new Uint8Array(0);
372
451
  const rowBytes = Math.ceil(columns / 8);
373
- const maxRows = rows > 0 ? rows : 10000;
452
+ const maxRows = Math.min(rows > 0 ? rows : 10000, Math.floor(reader.limits.maxImagePixels / columns));
374
453
  const output = [];
375
454
  const bitReader = new CCITTBitReader(data);
376
455
  if (k < 0) {
@@ -399,7 +478,9 @@ function decodeCCITTGroup3Row(reader, columns) {
399
478
  const line = new Uint8Array(columns);
400
479
  let col = 0;
401
480
  let isWhite = true;
402
- while (col < columns) {
481
+ // Past the end of the data every bit reads 0, which is no code at all, so a
482
+ // row cut short would otherwise alternate zero-length runs forever.
483
+ while (col < columns && !reader.exhausted) {
403
484
  const tree = isWhite ? whiteTree : blackTree;
404
485
  const runLen = ccittDecodeRun(reader, tree);
405
486
  const fill = isWhite ? 0 : 1;
@@ -575,13 +656,24 @@ function paethPredictor(a, b, c) {
575
656
  }
576
657
  // ── PDF structure parser ────────────────────────────────────────────────
577
658
  class PdfReader {
578
- constructor(buffer) {
659
+ /**
660
+ * `limits` holds the caps the reader and its renderers apply to what the
661
+ * file declares — image size, nesting, page count, form draws and total
662
+ * decoded bytes.
663
+ */
664
+ constructor(buffer, limits = DEFAULT_PARSER_LIMITS) {
665
+ this.limits = limits;
579
666
  this.objects = new Map();
580
667
  this.parsedObjects = new Map();
581
668
  this.compressedObjects = new Map();
582
669
  this.trailer = null;
670
+ /** References being parsed right now: an object whose /Length leads back to itself must not recurse. */
671
+ this.resolving = new Set();
672
+ /** Arrays and dictionaries open right now in the value being parsed. */
673
+ this.nesting = 0;
583
674
  this.data = new Uint8Array(buffer);
584
675
  this.text = new TextDecoder('latin1').decode(this.data);
676
+ this.decodeBudget = new DecodeBudget(limits.maxDecodedBytesTotal);
585
677
  }
586
678
  parse() {
587
679
  this.findXRef();
@@ -614,12 +706,21 @@ class PdfReader {
614
706
  match ??= this.findLastStartxref(this.text.slice(-8192));
615
707
  if (!match)
616
708
  throw new Error('Could not find startxref in PDF');
617
- const xrefOffset = Number.parseInt(match[1], 10);
618
- if (this.text.substring(xrefOffset, xrefOffset + 4) === 'xref') {
619
- this.parseTraditionalXRef(xrefOffset);
620
- }
621
- else {
622
- this.parseXRefStream(xrefOffset);
709
+ this.walkXRefChain(Number.parseInt(match[1], 10));
710
+ }
711
+ /**
712
+ * Reads the newest xref section, then each /Prev behind it. A section
713
+ * already read ends the walk, so a /Prev that points back at itself or at a
714
+ * later section cannot loop, and the walk needs no call stack however long
715
+ * the chain of incremental updates is.
716
+ */
717
+ walkXRefChain(newest) {
718
+ const seen = new Set();
719
+ let offset = newest;
720
+ while (offset !== null && visitOnce(seen, offset)) {
721
+ offset = this.text.substring(offset, offset + 4) === 'xref'
722
+ ? this.parseTraditionalXRef(offset)
723
+ : this.parseXRefStream(offset);
623
724
  }
624
725
  }
625
726
  /**
@@ -724,23 +825,27 @@ class PdfReader {
724
825
  this.objects.set(key, { offset: entryOffset, gen });
725
826
  }
726
827
  }
828
+ /** Adopts the section's trailer; returns the /Prev offset it names, if any. */
727
829
  parseTrailerDict(offset) {
728
830
  const trailerPos = this.text.indexOf('trailer', offset);
729
831
  if (trailerPos === -1)
730
- return;
832
+ return null;
731
833
  const dictStart = this.text.indexOf('<<', trailerPos);
732
834
  if (dictStart === -1)
733
- return;
835
+ return null;
734
836
  const result = this.parseObjectAt(dictStart);
735
837
  if (result.obj.type !== 'dict')
736
- return;
838
+ return null;
737
839
  const dict = result.obj.value;
738
840
  this.trailer ??= dict;
841
+ return this.prevOffset(dict);
842
+ }
843
+ prevOffset(dict) {
739
844
  const prev = dict['Prev'];
740
- if (prev?.type === 'number') {
741
- this.parseTraditionalXRef(prev.value);
742
- }
845
+ return prev?.type === 'number' ? prev.value : null;
743
846
  }
847
+ /** Smallest an entry can be: 10 + 1 + 5 + 1 + 1 characters, with no terminator. */
848
+ static { this.MIN_XREF_ENTRY_CHARS = 18; }
744
849
  parseTraditionalXRef(offset) {
745
850
  let pos = offset + 4;
746
851
  pos = this.skipWhitespace(pos);
@@ -754,14 +859,18 @@ class PdfReader {
754
859
  const count = Number.parseInt(headerMatch[2], 10);
755
860
  pos += headerMatch[0].length;
756
861
  pos = this.skipWhitespace(pos);
757
- for (let i = 0; i < count; i++) {
862
+ // The count is the file's claim; the bytes left bound what it can hold.
863
+ const holdable = Math.ceil((this.text.length - pos) / PdfReader.MIN_XREF_ENTRY_CHARS);
864
+ for (let i = 0; i < Math.min(count, holdable); i++) {
865
+ if (this.text.startsWith('trailer', pos))
866
+ break;
758
867
  this.parseXRefEntry(this.text.substring(pos, pos + 20), startObj, i);
759
868
  pos += 20;
760
869
  while (pos < this.text.length && (this.text[pos] === '\r' || this.text[pos] === '\n' || this.text[pos] === ' '))
761
870
  pos++;
762
871
  }
763
872
  }
764
- this.parseTrailerDict(offset);
873
+ return this.parseTrailerDict(offset);
765
874
  }
766
875
  readXRefField(decoded, bytePos, fieldOffset, fieldWidth) {
767
876
  let value = 0;
@@ -809,18 +918,13 @@ class PdfReader {
809
918
  }
810
919
  }
811
920
  }
812
- followPrevXRef(dict) {
813
- const prev = dict['Prev'];
814
- if (prev?.type !== 'number')
815
- return;
816
- const prevOffset = prev.value;
817
- if (this.text.substring(prevOffset, prevOffset + 4) === 'xref') {
818
- this.parseTraditionalXRef(prevOffset);
819
- }
820
- else {
821
- this.parseXRefStream(prevOffset);
822
- }
921
+ /** An xref stream entry is three fields of 0-8 bytes; all-zero widths would consume no input. */
922
+ static isValidXRefWidths(w) {
923
+ return w.length >= 3
924
+ && w.every(width => Number.isInteger(width) && width >= 0 && width <= 8)
925
+ && w[0] + w[1] + w[2] > 0;
823
926
  }
927
+ /** Adopts the section's entries and trailer; returns the /Prev offset it names, if any. */
824
928
  parseXRefStream(offset) {
825
929
  const result = this.parseObjectAt(offset);
826
930
  const obj = result.obj;
@@ -833,10 +937,12 @@ class PdfReader {
833
937
  if (wArr?.type !== 'array')
834
938
  throw new Error('Missing /W in xref stream');
835
939
  const w = wArr.value.map(o => o.value);
940
+ if (!PdfReader.isValidXRefWidths(w))
941
+ throw new Error('Invalid /W in xref stream');
836
942
  const indexArr = this.getXRefIndexArray(dict);
837
943
  const decoded = this.decodeStreamData(dict, streamData);
838
944
  this.processXRefStreamEntries(decoded, w, indexArr);
839
- this.followPrevXRef(dict);
945
+ return this.prevOffset(dict);
840
946
  }
841
947
  skipWhitespace(pos) {
842
948
  while (pos < this.text.length && ' \t\r\n\0\f'.includes(this.text[pos]))
@@ -847,10 +953,8 @@ class PdfReader {
847
953
  const lengthObj = dict['Length'];
848
954
  if (!lengthObj)
849
955
  return 0;
850
- if (lengthObj.type === 'ref') {
851
- return this.resolveRef(lengthObj).value;
852
- }
853
- return lengthObj.value;
956
+ const length = lengthObj.type === 'ref' ? this.resolveRef(lengthObj) : lengthObj;
957
+ return typeof length.value === 'number' ? length.value : 0;
854
958
  }
855
959
  parseObjectAt(offset) {
856
960
  let pos = offset;
@@ -890,7 +994,7 @@ class PdfReader {
890
994
  const ch = this.text[pos];
891
995
  const ch2 = pos + 1 < this.text.length ? this.text[pos + 1] : '';
892
996
  if (ch === '<' && ch2 === '<') {
893
- return this.parseDict(pos);
997
+ return this.parseNested(pos, () => this.parseDict(pos));
894
998
  }
895
999
  if (ch === '<') {
896
1000
  return this.parseHexString(pos);
@@ -902,7 +1006,7 @@ class PdfReader {
902
1006
  return this.parseName(pos);
903
1007
  }
904
1008
  if (ch === '[') {
905
- return this.parseArray(pos);
1009
+ return this.parseNested(pos, () => this.parseArray(pos));
906
1010
  }
907
1011
  if (ch === 't' && this.text.substring(pos, pos + 4) === 'true') {
908
1012
  return { obj: { type: 'boolean', value: true }, endPos: pos + 4 };
@@ -932,6 +1036,56 @@ class PdfReader {
932
1036
  }
933
1037
  return { obj: { type: 'number', value: num }, endPos: afterNum };
934
1038
  }
1039
+ /**
1040
+ * Parses the container at `pos` unless it would open past the nesting cap.
1041
+ * One that would is skipped whole and read as null: following it down costs
1042
+ * a stack frame per level, and a file can nest to its own length.
1043
+ */
1044
+ parseNested(pos, parse) {
1045
+ if (this.nesting >= this.limits.maxNestingDepth) {
1046
+ return { obj: { type: 'null', value: null }, endPos: this.skipContainer(pos) };
1047
+ }
1048
+ this.nesting++;
1049
+ try {
1050
+ return parse();
1051
+ }
1052
+ finally {
1053
+ this.nesting--;
1054
+ }
1055
+ }
1056
+ /** The end of the dictionary or array opening at `pos`, found without recursion. */
1057
+ skipContainer(pos) {
1058
+ let depth = 0;
1059
+ let i = pos;
1060
+ while (i < this.text.length) {
1061
+ const ch = this.text[i];
1062
+ const pair = ch + this.text.charAt(i + 1);
1063
+ if (ch === '(') {
1064
+ i = this.parseLiteralString(i).endPos;
1065
+ }
1066
+ else if (pair === '<<' || pair === '>>') {
1067
+ depth += pair === '<<' ? 1 : -1;
1068
+ i += 2;
1069
+ }
1070
+ else if (ch === '<') {
1071
+ // Not parseHexString: malformed hex is skipped, not decoded.
1072
+ const close = this.text.indexOf('>', i);
1073
+ i = close === -1 ? this.text.length : close + 1;
1074
+ }
1075
+ else {
1076
+ depth += PdfReader.bracketDelta(ch);
1077
+ i++;
1078
+ }
1079
+ if (depth <= 0)
1080
+ return i;
1081
+ }
1082
+ return i;
1083
+ }
1084
+ static bracketDelta(ch) {
1085
+ if (ch === '[')
1086
+ return 1;
1087
+ return ch === ']' ? -1 : 0;
1088
+ }
935
1089
  parseDict(pos) {
936
1090
  pos += 2;
937
1091
  const dict = {};
@@ -1115,9 +1269,17 @@ class PdfReader {
1115
1269
  this.parseObjectStream(compressed.streamObjNum);
1116
1270
  return this.parsedObjects.get(key) ?? { type: 'null', value: null };
1117
1271
  }
1118
- const result = this.parseObjectAt(entry.offset);
1119
- this.parsedObjects.set(key, result.obj);
1120
- return result.obj;
1272
+ if (this.resolving.has(key))
1273
+ return { type: 'null', value: null };
1274
+ this.resolving.add(key);
1275
+ try {
1276
+ const result = this.parseObjectAt(entry.offset);
1277
+ this.parsedObjects.set(key, result.obj);
1278
+ return result.obj;
1279
+ }
1280
+ finally {
1281
+ this.resolving.delete(key);
1282
+ }
1121
1283
  }
1122
1284
  /**
1123
1285
  * Follow a chain of indirect references to the object it ends at.
@@ -1236,6 +1398,8 @@ class PdfReader {
1236
1398
  const filterObj = dict['Filter'];
1237
1399
  if (!filterObj)
1238
1400
  return rawStream;
1401
+ if (this.decodeBudget.spent)
1402
+ return new Uint8Array(0);
1239
1403
  const filters = this.resolveFilterNames(filterObj);
1240
1404
  const decodeParms = this.getDecodeParmsArray(dict, filters.length);
1241
1405
  let result = rawStream;
@@ -1250,7 +1414,14 @@ class PdfReader {
1250
1414
  return rawStream;
1251
1415
  }
1252
1416
  }
1253
- return result;
1417
+ // A stream referenced from many places is decoded again at each one; the
1418
+ // budget is what keeps those repeats from adding up to gigabytes. Past
1419
+ // it the stream reads as empty, and the budget is spent so the next
1420
+ // reference is refused before it decodes anything.
1421
+ if (this.decodeBudget.take(result.length))
1422
+ return result;
1423
+ this.decodeBudget.spendRemainder();
1424
+ return new Uint8Array(0);
1254
1425
  }
1255
1426
  getStreamData(obj) {
1256
1427
  const resolved = this.resolveDeep(obj);
@@ -1296,21 +1467,32 @@ class PdfReader {
1296
1467
  return this.collectPages(pagesRef);
1297
1468
  }
1298
1469
  static { this.INHERITABLE_PAGE_KEYS = ['Resources', 'MediaBox', 'CropBox', 'Rotate']; }
1299
- collectPages(node, inherited = {}) {
1300
- const dict = this.getDict(node);
1301
- const typeObj = dict['Type'];
1302
- const typeName = typeObj ? this.getString(typeObj) : '';
1303
- const kids = dict['Kids'];
1304
- if (typeName === 'Page' || (!kids && dict['Contents'])) {
1305
- this.applyInheritedPageKeys(dict, inherited);
1306
- return [this.resolveDeep(node)];
1307
- }
1308
- if (!kids)
1309
- return [];
1310
- const nextInherited = this.mergeInheritablePageKeys(dict, inherited);
1470
+ /**
1471
+ * Walks the page tree depth-first, in page order. Each node is visited once,
1472
+ * so a /Kids that lists an ancestor, or the same child many times, yields
1473
+ * every distinct page once; the walk keeps its own stack, so a tree of any
1474
+ * depth costs no call stack.
1475
+ */
1476
+ collectPages(root) {
1311
1477
  const pages = [];
1312
- for (const kid of this.getArray(kids)) {
1313
- pages.push(...this.collectPages(kid, nextInherited));
1478
+ const seen = new Set();
1479
+ const pending = [{ node: root, inherited: {} }];
1480
+ for (let next = pending.pop(); next && pages.length < this.limits.maxPages; next = pending.pop()) {
1481
+ const resolved = this.resolveDeep(next.node);
1482
+ if (!visitOnce(seen, resolved))
1483
+ continue;
1484
+ const dict = this.getDict(resolved);
1485
+ const kids = dict['Kids'];
1486
+ if (this.getString(dict['Type']) === 'Page' || (!kids && dict['Contents'])) {
1487
+ this.applyInheritedPageKeys(dict, next.inherited);
1488
+ pages.push(resolved);
1489
+ }
1490
+ else if (kids) {
1491
+ const inherited = this.mergeInheritablePageKeys(dict, next.inherited);
1492
+ // Pushed last-first, so the first kid is popped first.
1493
+ for (const kid of this.getArray(kids).reverse())
1494
+ pending.push({ node: kid, inherited });
1495
+ }
1314
1496
  }
1315
1497
  return pages;
1316
1498
  }
@@ -1417,11 +1599,32 @@ function pdfStringToUnicode(raw, fontInfo) {
1417
1599
  const { result, charCodes } = decodeSingleByteString(raw, fontInfo?.toUnicode);
1418
1600
  return { text: result, charCodes };
1419
1601
  }
1602
+ const WHITESPACE = /\s/;
1603
+ /**
1604
+ * The text between each `begin` marker (plus the one whitespace character that
1605
+ * must follow it) and the next `end`, in order. Found with indexOf: a lazy
1606
+ * regex retries from every unterminated `begin` and rescans to the end of the
1607
+ * text each time, which is quadratic. Once no `end` is left, no later section
1608
+ * can close either, so the scan stops.
1609
+ */
1610
+ function* markedSections(text, begin, end) {
1611
+ let from = 0;
1612
+ for (let start = text.indexOf(begin, from); start !== -1; start = text.indexOf(begin, from)) {
1613
+ const bodyStart = start + begin.length + 1;
1614
+ if (!WHITESPACE.test(text.charAt(bodyStart - 1))) {
1615
+ from = start + 1;
1616
+ continue;
1617
+ }
1618
+ const close = text.indexOf(end, bodyStart);
1619
+ if (close === -1)
1620
+ return;
1621
+ yield text.slice(bodyStart, close);
1622
+ from = close + end.length;
1623
+ }
1624
+ }
1420
1625
  function parseBfCharEntries(text, map) {
1421
- const bfCharRe = /beginbfchar\s([\s\S]*?)endbfchar/g;
1422
- let match;
1423
- while ((match = bfCharRe.exec(text)) !== null) {
1424
- for (const entry of match[1].trim().split('\n')) {
1626
+ for (const section of markedSections(text, 'beginbfchar', 'endbfchar')) {
1627
+ for (const entry of section.trim().split('\n')) {
1425
1628
  const parts = /<([0-9a-fA-F]+)>\s*<([0-9a-fA-F]+)>/.exec(entry.trim());
1426
1629
  if (!parts)
1427
1630
  continue;
@@ -1468,10 +1671,8 @@ function parseBfRangeEntry(trimmed, map) {
1468
1671
  }
1469
1672
  }
1470
1673
  function parseBfRangeEntries(text, map) {
1471
- const bfRangeRe = /beginbfrange\s([\s\S]*?)endbfrange/g;
1472
- let match;
1473
- while ((match = bfRangeRe.exec(text)) !== null) {
1474
- for (const entry of match[1].trim().split('\n')) {
1674
+ for (const section of markedSections(text, 'beginbfrange', 'endbfrange')) {
1675
+ for (const entry of section.trim().split('\n')) {
1475
1676
  parseBfRangeEntry(entry.trim(), map);
1476
1677
  }
1477
1678
  }
@@ -2709,9 +2910,15 @@ function buildFormCtm(ctx, dict) {
2709
2910
  const m = matrixArr.map(o => ctx.reader.getNumber(o));
2710
2911
  return multiplyMatrix$1(m, ctx.gs.ctm);
2711
2912
  }
2913
+ /**
2914
+ * Draws a form XObject unless it is already being drawn higher up (a cycle),
2915
+ * the path is at the nesting cap, or the page's draw budget is spent. A form
2916
+ * shared between sibling draws is drawn every time.
2917
+ */
2712
2918
  function processFormXObject(parentCtx, formObj) {
2713
- if (parentCtx.formDepth >= 10)
2714
- return;
2919
+ parentCtx.formDraws.enter(formObj, () => drawFormXObject(parentCtx, formObj));
2920
+ }
2921
+ function drawFormXObject(parentCtx, formObj) {
2715
2922
  const dict = formObj.value;
2716
2923
  if (!formObj.stream)
2717
2924
  return;
@@ -2745,7 +2952,7 @@ function buildFormContext(parentCtx, fontInfoMap, xObjectDict, resources, formCt
2745
2952
  lineCap: parentCtx.gs.lineCap, lineJoin: parentCtx.gs.lineJoin,
2746
2953
  },
2747
2954
  operandStack: [], pageIndex: parentCtx.pageIndex,
2748
- formDepth: parentCtx.formDepth + 1, compatibilityMode: 0,
2955
+ formDraws: parentCtx.formDraws, compatibilityMode: 0,
2749
2956
  mcidStack: [], ocgOffSet: parentCtx.ocgOffSet ?? new Set(),
2750
2957
  ocgHiddenDepth: 0, perFragment: parentCtx.perFragment,
2751
2958
  patternFillImage: null,
@@ -3739,7 +3946,7 @@ function buildExtractionContext(reader, resources, fontDict, xObjectDict, initia
3739
3946
  dashArray: [], dashPhase: 0, lineCap: 0, lineJoin: 0,
3740
3947
  },
3741
3948
  operandStack: [], pageIndex,
3742
- formDepth: 0, compatibilityMode: 0,
3949
+ formDraws: new DrawPath(reader.limits.maxNestingDepth, reader.limits.maxFormDrawsPerPage), compatibilityMode: 0,
3743
3950
  mcidStack: [], ocgOffSet: new Set(),
3744
3951
  ocgHiddenDepth: 0, perFragment: options?.perFragment ?? false,
3745
3952
  patternFillImage: null,
@@ -3795,6 +4002,16 @@ function resolveImageFilterName(reader, dict) {
3795
4002
  const last = reader.resolveDeep(lastEntry);
3796
4003
  return last.type === 'name' ? last.value : '';
3797
4004
  }
4005
+ const ALLOWED_BITS_PER_COMPONENT = new Set([1, 2, 4, 8, 16]);
4006
+ /**
4007
+ * Whether the declared raster is one we will expand into pixel rows, PNG and
4008
+ * base64 (roughly 24 bytes per source byte). The size is the file's claim, so it
4009
+ * is checked before anything is allocated for it.
4010
+ */
4011
+ function isDecodableRaster(reader, width, height, dict) {
4012
+ const bpc = reader.getNumber(dict['BitsPerComponent']) || 8;
4013
+ return ALLOWED_BITS_PER_COMPONENT.has(bpc) && width * height <= reader.limits.maxImagePixels;
4014
+ }
3798
4015
  function buildImageDataUrl(filterName, xObj, reader, width, height, dict) {
3799
4016
  if (filterName === 'DCTDecode') {
3800
4017
  if (!xObj.stream)
@@ -3806,6 +4023,8 @@ function buildImageDataUrl(filterName, xObj, reader, width, height, dict) {
3806
4023
  return null;
3807
4024
  return `data:image/jp2;base64,${uint8ArrayToBase64(xObj.stream)}`;
3808
4025
  }
4026
+ if (!isDecodableRaster(reader, width, height, dict))
4027
+ return null;
3809
4028
  const decoded = reader.getStreamData(xObj);
3810
4029
  if (decoded.length === 0)
3811
4030
  return null;
@@ -4620,7 +4839,7 @@ function buildItemStyles(item, bodyFont, bodySize) {
4620
4839
  if (!isDefaultColor(item.color))
4621
4840
  styles.push(`color: ${item.color}`);
4622
4841
  if (item.fontFamily && item.fontFamily !== bodyFont)
4623
- styles.push(`font-family: '${item.fontFamily}'`);
4842
+ styles.push(`font-family: ${cssFontFamily(item.fontFamily)}`);
4624
4843
  const sizeRatio = bodySize > 0 ? item.fontSize / bodySize : 1;
4625
4844
  if (sizeRatio < 0.85 || sizeRatio > 1.15) {
4626
4845
  const editorSize = Math.round(sizeRatio * 12);
@@ -4642,7 +4861,7 @@ function wrapItemHtml(item, text, bodyFont, bodySize, linkUri) {
4642
4861
  let fragment = text;
4643
4862
  const styles = buildItemStyles(item, bodyFont, bodySize);
4644
4863
  if (styles.length > 0) {
4645
- fragment = `<span style="${styles.join('; ')}">${fragment}</span>`;
4864
+ fragment = `<span style="${escapeAttribute(styles.join('; '))}">${fragment}</span>`;
4646
4865
  }
4647
4866
  if (item.textRise > 1) {
4648
4867
  fragment = `<sup>${fragment}</sup>`;
@@ -4654,10 +4873,10 @@ function wrapItemHtml(item, text, bodyFont, bodySize, linkUri) {
4654
4873
  fragment = `<em>${fragment}</em>`;
4655
4874
  if (item.bold)
4656
4875
  fragment = `<strong>${fragment}</strong>`;
4657
- if (linkUri) {
4658
- const safeUri = escapeHtml$2(linkUri);
4876
+ const safeUri = linkUri ? safeLinkHref(linkUri) : undefined;
4877
+ if (safeUri !== undefined) {
4659
4878
  const linkColor = isDefaultColor(item.color) ? '#1a0dab' : item.color;
4660
- fragment = `<a href="${safeUri}" target="_blank" rel="noopener noreferrer" style="color:${linkColor};text-decoration:underline">${fragment}</a>`;
4879
+ fragment = `<a href="${escapeAttribute(safeUri)}" target="_blank" rel="noopener noreferrer" style="color:${linkColor};text-decoration:underline">${fragment}</a>`;
4661
4880
  }
4662
4881
  return fragment;
4663
4882
  }
@@ -4756,7 +4975,21 @@ function detectColumns(lines) {
4756
4975
  const BULLET_PATTERN = /^[\u2022\u2023\u25E6\u2043\u2219\u25CF\u25CB\u25AA\u25AB\u2013\u2014\-*]\s*/;
4757
4976
  const BULLET_CHARS = /^[\u2022\u2023\u25E6\u2043\u2219\u25CF\u25CB\u25AA\u25AB\u2013\u2014\-*]+$/;
4758
4977
  const NUMBERED_PATTERN = /^(\d{1,3})[.)]\s+/;
4759
- const NUMBERED_PATTERN_RTL_END = /(?=(\s+))\1[.)]\d{1,3}$/;
4978
+ const RTL_NUMBER_TAIL = /[.)]\d{1,3}$/;
4979
+ /**
4980
+ * Where a trailing right-to-left list number starts — whitespace, then ".12"
4981
+ * or ")12" at the end of the text — or -1. Matched from the number backwards:
4982
+ * a `\s+` regex retried from every space of a long run is quadratic.
4983
+ */
4984
+ function rtlNumberTailStart(text) {
4985
+ const tail = RTL_NUMBER_TAIL.exec(text);
4986
+ if (!tail)
4987
+ return -1;
4988
+ let start = tail.index;
4989
+ while (start > 0 && WHITESPACE.test(text.charAt(start - 1)))
4990
+ start--;
4991
+ return start === tail.index ? -1 : start;
4992
+ }
4760
4993
  function stripBulletItems(line) {
4761
4994
  const filtered = line.items.filter(item => {
4762
4995
  const t = item.text.trim();
@@ -4795,9 +5028,9 @@ function stripNumberedPrefix(line) {
4795
5028
  const trimmed = item.text.replace(NUMBERED_PATTERN, '');
4796
5029
  if (trimmed !== item.text)
4797
5030
  return { ...item, text: trimmed };
4798
- const trimmedRtl = item.text.replace(NUMBERED_PATTERN_RTL_END, '');
4799
- if (trimmedRtl !== item.text)
4800
- return { ...item, text: trimmedRtl };
5031
+ const rtlStart = rtlNumberTailStart(item.text);
5032
+ if (rtlStart >= 0)
5033
+ return { ...item, text: item.text.slice(0, rtlStart) };
4801
5034
  return item;
4802
5035
  }),
4803
5036
  y: line.y,
@@ -4980,7 +5213,7 @@ function appendToParagraph(line, bodySize, bodyFont, state, options, rtl) {
4980
5213
  detectParagraphIndent(state);
4981
5214
  }
4982
5215
  function isNumberedLine(lineText, rtl) {
4983
- return NUMBERED_PATTERN.test(lineText) || (rtl && NUMBERED_PATTERN_RTL_END.test(lineText));
5216
+ return NUMBERED_PATTERN.test(lineText) || (rtl && rtlNumberTailStart(lineText) >= 0);
4984
5217
  }
4985
5218
  function processHtmlLine(line, lineText, bodySize, bodyFont, state, options) {
4986
5219
  const { structureMap } = options;
@@ -5791,39 +6024,44 @@ function resolvePageIndex(reader, pageRef, pages) {
5791
6024
  return findPageIndexByDict(reader, dict, pages);
5792
6025
  }
5793
6026
  function processStructKDict(opts) {
5794
- const { reader, kDict, pages, effectivePage, depth, mcids, children, elemRef } = opts;
6027
+ const { walk, kDict, effectivePage, depth, mcids, children, elemRef } = opts;
5795
6028
  const mcidObj = kDict['MCID'];
5796
6029
  if (mcidObj) {
5797
- const mcidVal = reader.getNumber(mcidObj);
6030
+ const mcidVal = walk.reader.getNumber(mcidObj);
5798
6031
  const mcidPageRef = kDict['Pg'];
5799
- const mcidPage = mcidPageRef ? resolvePageIndex(reader, mcidPageRef, pages) : effectivePage;
6032
+ const mcidPage = mcidPageRef ? resolvePageIndex(walk.reader, mcidPageRef, walk.pages) : effectivePage;
5800
6033
  mcids.push({ mcid: mcidVal, page: mcidPage >= 0 ? mcidPage : effectivePage });
5801
6034
  }
5802
6035
  else {
5803
- const child = parseStructureElement(reader, elemRef, pages, effectivePage, depth + 1);
6036
+ const child = parseStructureElement(walk, elemRef, effectivePage, depth + 1);
5804
6037
  if (child)
5805
6038
  children.push(child);
5806
6039
  }
5807
6040
  }
5808
- function processStructKArrayItem(reader, item, pages, effectivePage, depth, mcids, children) {
5809
- const itemResolved = reader.resolveDeep(item);
6041
+ function processStructKArrayItem(walk, item, effectivePage, depth, mcids, children) {
6042
+ const itemResolved = walk.reader.resolveDeep(item);
5810
6043
  if (itemResolved.type === 'number') {
5811
6044
  mcids.push({ mcid: itemResolved.value, page: effectivePage });
5812
6045
  }
5813
6046
  else if (itemResolved.type === 'dict') {
5814
6047
  const itemDict = itemResolved.value;
5815
- processStructKDict({ reader, kDict: itemDict, pages, effectivePage, depth, mcids, children, elemRef: item });
6048
+ processStructKDict({ walk, kDict: itemDict, effectivePage, depth, mcids, children, elemRef: item });
5816
6049
  }
5817
6050
  }
5818
- function parseStructureElement(reader, elemObj, pages, parentPageIdx, depth) {
6051
+ function parseStructureElement(walk, elemObj, parentPageIdx, depth) {
5819
6052
  if (depth > 50)
5820
6053
  return null;
6054
+ const { reader, pages } = walk;
5821
6055
  const resolved = reader.resolveDeep(elemObj);
5822
6056
  if (resolved.type === 'number') {
5823
6057
  return { type: 'MCID', mcids: [{ mcid: resolved.value, page: parentPageIdx }], children: [] };
5824
6058
  }
5825
6059
  if (resolved.type !== 'dict')
5826
6060
  return null;
6061
+ // An element has one parent; a second reference to it is a shared or cyclic
6062
+ // node, and expanding it again costs a copy of its whole subtree each time.
6063
+ if (!visitOnce(walk.seen, resolved))
6064
+ return null;
5827
6065
  const dict = resolved.value;
5828
6066
  const typeStr = reader.getString(dict['Type']);
5829
6067
  if (typeStr && typeStr !== 'StructElem')
@@ -5843,11 +6081,11 @@ function parseStructureElement(reader, elemObj, pages, parentPageIdx, depth) {
5843
6081
  }
5844
6082
  else if (kResolved.type === 'dict') {
5845
6083
  const kDict = kResolved.value;
5846
- processStructKDict({ reader, kDict, pages, effectivePage, depth, mcids, children, elemRef: kResolved });
6084
+ processStructKDict({ walk, kDict, effectivePage, depth, mcids, children, elemRef: kResolved });
5847
6085
  }
5848
6086
  else if (kResolved.type === 'array') {
5849
6087
  for (const item of kResolved.value) {
5850
- processStructKArrayItem(reader, item, pages, effectivePage, depth, mcids, children);
6088
+ processStructKArrayItem(walk, item, effectivePage, depth, mcids, children);
5851
6089
  }
5852
6090
  }
5853
6091
  return { type: sType, mcids, children };
@@ -5863,8 +6101,8 @@ function collectMcidTypes(node, result) {
5863
6101
  collectMcidTypes(child, result);
5864
6102
  }
5865
6103
  }
5866
- function collectMcidTypesFromElement(reader, item, pages, mcidToType) {
5867
- const node = parseStructureElement(reader, item, pages, 0, 0);
6104
+ function collectMcidTypesFromElement(walk, item, mcidToType) {
6105
+ const node = parseStructureElement(walk, item, 0, 0);
5868
6106
  if (node)
5869
6107
  collectMcidTypes(node, mcidToType);
5870
6108
  }
@@ -5880,14 +6118,15 @@ function parseStructureTree(reader, pages) {
5880
6118
  if (!kObj)
5881
6119
  return emptyResult;
5882
6120
  const mcidToType = new Map();
6121
+ const walk = { reader, pages, seen: new Set() };
5883
6122
  const kResolved = reader.resolveDeep(kObj);
5884
6123
  if (kResolved.type === 'array') {
5885
6124
  for (const item of kResolved.value) {
5886
- collectMcidTypesFromElement(reader, item, pages, mcidToType);
6125
+ collectMcidTypesFromElement(walk, item, mcidToType);
5887
6126
  }
5888
6127
  }
5889
6128
  else {
5890
- collectMcidTypesFromElement(reader, kObj, pages, mcidToType);
6129
+ collectMcidTypesFromElement(walk, kObj, mcidToType);
5891
6130
  }
5892
6131
  return { mcidToType, hasStructure: mcidToType.size > 0 };
5893
6132
  }
@@ -5924,7 +6163,11 @@ function isStructureListItem(type) {
5924
6163
  return type === 'LI' || type === 'LBody' || type === 'Lbl';
5925
6164
  }
5926
6165
  // ── Main export ─────────────────────────────────────────────────────────
5927
- async function parsePdf(buffer) {
6166
+ /**
6167
+ * Parses a PDF into flowing HTML and plain text.
6168
+ * Input is untrusted; structures exceeding internal limits are skipped.
6169
+ */
6170
+ async function parsePdf(buffer, limits = DEFAULT_PARSER_LIMITS) {
5928
6171
  const header = new Uint8Array(buffer, 0, Math.min(5, buffer.byteLength));
5929
6172
  if (header.length < 5 ||
5930
6173
  header[0] !== 0x25 ||
@@ -5934,7 +6177,7 @@ async function parsePdf(buffer) {
5934
6177
  header[4] !== 0x2D) {
5935
6178
  throw new Error('Not a valid PDF file.');
5936
6179
  }
5937
- const reader = new PdfReader(buffer);
6180
+ const reader = new PdfReader(buffer, limits);
5938
6181
  try {
5939
6182
  reader.parse();
5940
6183
  }
@@ -6036,14 +6279,18 @@ function extractAllPages(reader, pdfPages, structureMap) {
6036
6279
  }
6037
6280
  return { pages, allTextParts, allHtmlParts, allImageOnly };
6038
6281
  }
6039
- async function parsePdfPaged(buffer) {
6282
+ /**
6283
+ * Parses a PDF into one HTML fragment per page, plus the outline.
6284
+ * Input is untrusted; structures exceeding internal limits are skipped.
6285
+ */
6286
+ async function parsePdfPaged(buffer, limits = DEFAULT_PARSER_LIMITS) {
6040
6287
  const header = new Uint8Array(buffer, 0, Math.min(5, buffer.byteLength));
6041
6288
  if (header.length < 5 ||
6042
6289
  header[0] !== 0x25 || header[1] !== 0x50 || header[2] !== 0x44 ||
6043
6290
  header[3] !== 0x46 || header[4] !== 0x2D) {
6044
6291
  throw new Error('Not a valid PDF file.');
6045
6292
  }
6046
- const reader = new PdfReader(buffer);
6293
+ const reader = new PdfReader(buffer, limits);
6047
6294
  try {
6048
6295
  reader.parse();
6049
6296
  }
@@ -6080,7 +6327,7 @@ function parseOutline(reader, pdfPages) {
6080
6327
  if (!firstRef)
6081
6328
  return [];
6082
6329
  const pageMap = buildPageObjNumMap(reader, pdfPages);
6083
- return parseOutlineItems(reader, firstRef, pageMap, 0);
6330
+ return parseOutlineItems(reader, firstRef, pageMap, 0, new Set());
6084
6331
  }
6085
6332
  catch {
6086
6333
  return [];
@@ -6131,11 +6378,14 @@ function resolveOutlinePageIndex(reader, item, pageMap) {
6131
6378
  return -1;
6132
6379
  }
6133
6380
  }
6134
- function parseOutlineItems(reader, firstRef, pageMap, depth) {
6381
+ /**
6382
+ * `visited` spans the whole outline tree, not one sibling list: two items
6383
+ * pointing at the same child list would otherwise each expand it in full.
6384
+ */
6385
+ function parseOutlineItems(reader, firstRef, pageMap, depth, visited) {
6135
6386
  if (depth > 10)
6136
6387
  return [];
6137
6388
  const items = [];
6138
- const visited = new Set();
6139
6389
  let currentRef = firstRef;
6140
6390
  while (currentRef) {
6141
6391
  const key = currentRef.type === 'ref' ? currentRef.value : '';
@@ -6150,7 +6400,7 @@ function parseOutlineItems(reader, firstRef, pageMap, depth) {
6150
6400
  let children = [];
6151
6401
  const childFirst = itemDict['First'];
6152
6402
  if (childFirst) {
6153
- children = parseOutlineItems(reader, childFirst, pageMap, depth + 1);
6403
+ children = parseOutlineItems(reader, childFirst, pageMap, depth + 1, visited);
6154
6404
  }
6155
6405
  items.push({ title, pageIndex, children });
6156
6406
  const nextRef = itemDict['Next'];
@@ -7247,6 +7497,42 @@ function escapeHtml$1(str) {
7247
7497
  .replaceAll('>', '&gt;')
7248
7498
  .replaceAll('"', '&quot;');
7249
7499
  }
7500
+ /**
7501
+ * A font name taken from the PDF as a quoted CSS string. The CSS is emitted inside
7502
+ * a `<style>` element, so quotes and backslashes are escaped, control characters
7503
+ * dropped and `<` hex-escaped: nothing in the name can end the string, the rule or
7504
+ * the style element.
7505
+ */
7506
+ function cssQuotedFontName(name) {
7507
+ let quoted = '';
7508
+ for (const ch of name) {
7509
+ const code = ch.codePointAt(0) ?? 0;
7510
+ if (code < 0x20 || code === 0x7f)
7511
+ continue;
7512
+ if (ch === '\\' || ch === "'")
7513
+ quoted += `\\${ch}`;
7514
+ else if (ch === '<')
7515
+ quoted += String.raw `\3c `;
7516
+ else
7517
+ quoted += ch;
7518
+ }
7519
+ return `'${quoted}'`;
7520
+ }
7521
+ const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:']);
7522
+ /**
7523
+ * The link annotation's URI as the browser will parse it when it is an absolute
7524
+ * http, https or mailto URL; `undefined` for any other scheme or a relative URI.
7525
+ */
7526
+ function safeLinkUri(uri) {
7527
+ try {
7528
+ const url = new URL(uri);
7529
+ return SAFE_LINK_PROTOCOLS.has(url.protocol) ? url.href : undefined;
7530
+ }
7531
+ catch {
7532
+ // Not an absolute URL; a relative URI has no page to resolve against here.
7533
+ return undefined;
7534
+ }
7535
+ }
7250
7536
  function resolveFontDescriptor(reader, fontObjDict) {
7251
7537
  const direct = fontObjDict['FontDescriptor'];
7252
7538
  if (direct)
@@ -7537,6 +7823,21 @@ function buildFallbackGlyph(fb, unicode, targetAdvance, unitsPerEm) {
7537
7823
  // Serif font family keywords for generic fallback detection
7538
7824
  const SERIF_KEYWORDS = ['times', 'georgia', 'cambria', 'garamond', 'palatino', 'bodoni', 'baskerville', 'bookman', 'century'];
7539
7825
  const MONO_KEYWORDS = ['courier', 'mono', 'consolas', 'menlo', 'lucidaconsole', 'inconsolata'];
7826
+ const STYLE_SUFFIX = /(?:Bold|Italic|Regular|Medium|Light|Condensed|Black|Heavy|Oblique|Book|Demi|Semi|Ultra|Thin|Extra|PS)$/i;
7827
+ /**
7828
+ * Drops one trailing style word and the whitespace before it ("Arial Bold" →
7829
+ * "Arial"). Found from the word backwards: a `\s+` regex retried from every
7830
+ * space of a long run of spaces is quadratic.
7831
+ */
7832
+ function stripStyleSuffix(name) {
7833
+ const word = STYLE_SUFFIX.exec(name);
7834
+ if (!word)
7835
+ return name;
7836
+ let end = word.index;
7837
+ while (end > 0 && /\s/.test(name.charAt(end - 1)))
7838
+ end--;
7839
+ return end === word.index ? name : name.slice(0, end);
7840
+ }
7540
7841
  /**
7541
7842
  * Dynamically extract a CSS font-family from any PDF BaseFontName.
7542
7843
  * No static map needed — works for any font by parsing the name.
@@ -7561,9 +7862,8 @@ function getSystemFontFamily(baseFontName) {
7561
7862
  // Clean up dashes/underscores/commas to spaces
7562
7863
  name = name.replaceAll(/[-_,]+/g, ' ').trim();
7563
7864
  // Remove style/weight suffixes (run twice to catch consecutive: "PS Bold" → "")
7564
- const stylePat = /(?=(\s+))\1(Bold|Italic|Regular|Medium|Light|Condensed|Black|Heavy|Oblique|Book|Demi|Semi|Ultra|Thin|Extra|PS)$/gi;
7565
- name = name.replaceAll(stylePat, '').trim();
7566
- name = name.replaceAll(stylePat, '').trim();
7865
+ name = stripStyleSuffix(name).trim();
7866
+ name = stripStyleSuffix(name).trim();
7567
7867
  // Title-case all-lowercase names: "arialuni" → "Arialuni"
7568
7868
  if (name === name.toLowerCase() && name.length > 0) {
7569
7869
  name = name[0].toUpperCase() + name.slice(1);
@@ -7575,7 +7875,7 @@ function getSystemFontFamily(baseFontName) {
7575
7875
  generic = 'serif';
7576
7876
  else if (MONO_KEYWORDS.some(k => lower.includes(k)))
7577
7877
  generic = 'monospace';
7578
- return `'${name}', ${generic}`;
7878
+ return `${cssQuotedFontName(name)}, ${generic}`;
7579
7879
  }
7580
7880
  function usesSystemFallback(entry) {
7581
7881
  return !entry.reEncodedData;
@@ -7644,7 +7944,7 @@ function fallbackChainFor(entry) {
7644
7944
  generic = 'monospace';
7645
7945
  else if (FAMILY_SERIF_HINT.test(name))
7646
7946
  generic = 'serif';
7647
- const original = entry.familyName ? `'${entry.familyName.replaceAll(/['";]/g, '')}',` : '';
7947
+ const original = entry.familyName ? `${cssQuotedFontName(entry.familyName)},` : '';
7648
7948
  return `${original}${generic}`;
7649
7949
  }
7650
7950
  function resolveFontFamily(entry, isSymbolFont, familyPrefix = '') {
@@ -7660,7 +7960,7 @@ function resolveFontFamily(entry, isSymbolFont, familyPrefix = '') {
7660
7960
  if (entry.fontData && fontFormatToMime(entry.fontData.format)) {
7661
7961
  return `${embeddedId},${fallbackChainFor(entry)}`;
7662
7962
  }
7663
- return entry.familyName || 'serif';
7963
+ return entry.familyName ? cssQuotedFontName(entry.familyName) : 'serif';
7664
7964
  }
7665
7965
  class FontRegistry {
7666
7966
  constructor(familyPrefix = '') {
@@ -8785,12 +9085,18 @@ class PixelPerfectProcessor {
8785
9085
  this.reader = reader;
8786
9086
  this.fontRegistry = fontRegistry;
8787
9087
  this.zoom = zoom;
9088
+ this.formDraws = this.newFormDraws();
8788
9089
  // general.cc:326-327: text_scale_factor1 = max(zoom, font_size_multiplier)
8789
9090
  this.textScaleFactor1 = Math.max(zoom, FONT_SIZE_MULTIPLIER);
8790
9091
  this.textScaleFactor2 = zoom / this.textScaleFactor1;
8791
9092
  }
9093
+ newFormDraws() {
9094
+ const { maxNestingDepth, maxFormDrawsPerPage } = this.reader.limits;
9095
+ return new DrawPath(maxNestingDepth, maxFormDrawsPerPage);
9096
+ }
8792
9097
  processPage(pageObj, _pageIndex) {
8793
9098
  this.resetState();
9099
+ this.formDraws = this.newFormDraws();
8794
9100
  this.lines.length = 0;
8795
9101
  this.pathRects.length = 0;
8796
9102
  this.imageItems.length = 0;
@@ -9571,13 +9877,13 @@ class PixelPerfectProcessor {
9571
9877
  const objDict = this.reader.getDict(ref);
9572
9878
  const subtype = this.reader.getString(objDict['Subtype']);
9573
9879
  if (subtype === 'Form') {
9574
- this.processFormXObject(ref, resources, pageWidth, pageHeight);
9880
+ this.formDraws.enter(this.reader.resolveDeep(ref), () => this.drawFormXObject(ref, resources, pageWidth, pageHeight));
9575
9881
  }
9576
9882
  else if (subtype === 'Image') {
9577
9883
  this.processImageXObject(ref, objDict);
9578
9884
  }
9579
9885
  }
9580
- processFormXObject(ref, parentResources, pageWidth, pageHeight) {
9886
+ drawFormXObject(ref, parentResources, pageWidth, pageHeight) {
9581
9887
  const objDict = this.reader.getDict(ref);
9582
9888
  const formResources = objDict['Resources']
9583
9889
  ? this.reader.getDict(objDict['Resources'])
@@ -9864,14 +10170,17 @@ function renderImage(img, z) {
9864
10170
  const w = round$1(img.renderWidth * z);
9865
10171
  const h = round$1(img.renderHeight * z);
9866
10172
  return `<img class="bi" style="position:absolute;left:${left}px;bottom:${bottom}px;` +
9867
- `width:${w}px;height:${h}px;" src="${img.dataUrl}"/>`;
10173
+ `width:${w}px;height:${h}px;" src="${escapeHtml$1(img.dataUrl)}"/>`;
9868
10174
  }
9869
10175
  function renderAnnotation(ann, z) {
10176
+ const href = safeLinkUri(ann.uri);
10177
+ if (!href)
10178
+ return '';
9870
10179
  const left = round$1(ann.x * z);
9871
10180
  const bottom = round$1(ann.y * z);
9872
10181
  const w = round$1(ann.width * z);
9873
10182
  const h = round$1(ann.height * z);
9874
- return `<a class="l" href="${escapeHtml$1(ann.uri)}">` +
10183
+ return `<a class="l" href="${escapeHtml$1(href)}">` +
9875
10184
  `<div style="position:absolute;left:${left}px;bottom:${bottom}px;` +
9876
10185
  `width:${w}px;height:${h}px;background-color:rgba(255,255,255,0.000001);border-style:none;"></div></a>`;
9877
10186
  }
@@ -9957,14 +10266,18 @@ function assemblePageHtml(result, pageIndex, mgr, fontRegistry, z) {
9957
10266
  };
9958
10267
  }
9959
10268
  // ── Main Entry Point ──────────────────────────────────────────────────
9960
- async function renderPixelPerfectPaged(buffer, options) {
10269
+ /**
10270
+ * Renders a PDF page by page with absolutely positioned text, for pixel-faithful display.
10271
+ * Input is untrusted; structures exceeding internal limits are skipped.
10272
+ */
10273
+ async function renderPixelPerfectPaged(buffer, options, limits = DEFAULT_PARSER_LIMITS) {
9961
10274
  const header = new Uint8Array(buffer, 0, Math.min(5, buffer.byteLength));
9962
10275
  if (header.length < 5 ||
9963
10276
  header[0] !== 0x25 || header[1] !== 0x50 || header[2] !== 0x44 ||
9964
10277
  header[3] !== 0x46 || header[4] !== 0x2D) {
9965
10278
  throw new Error('Not a valid PDF file.');
9966
10279
  }
9967
- const reader = new PdfReader(buffer);
10280
+ const reader = new PdfReader(buffer, limits);
9968
10281
  try {
9969
10282
  reader.parse();
9970
10283
  }
@@ -14365,8 +14678,9 @@ function emitRun(run, dominant, ctx) {
14365
14678
  html = `<sup>${html}</sup>`;
14366
14679
  if (style.baselineShift === 'sub')
14367
14680
  html = `<sub>${html}</sub>`;
14368
- if (style.link)
14369
- html = `<a href="${escapeHtml(style.link)}" target="_blank" rel="noopener">${html}</a>`;
14681
+ const href = style.link ? safeLinkHref(style.link) : undefined;
14682
+ if (href !== undefined)
14683
+ html = `<a href="${escapeHtml(href)}" target="_blank" rel="noopener">${html}</a>`;
14370
14684
  return html;
14371
14685
  }
14372
14686
  function dominantRunStyle(lines) {
@@ -14493,11 +14807,17 @@ function colorValue(color) {
14493
14807
  const normalized = color.toLowerCase();
14494
14808
  return normalized === '#000000' || normalized === '#000' ? '' : color;
14495
14809
  }
14810
+ /**
14811
+ * The declarations as the VALUE of a double-quoted `style` attribute. Escaped
14812
+ * here, at the one place every style attribute is built, because a font name
14813
+ * can hold `&#39&#59…` (character references without their semicolon), which a
14814
+ * browser decodes back into a quote and a `;` and so into a further declaration.
14815
+ */
14496
14816
  function styleAttr(pairs) {
14497
- return pairs
14817
+ return escapeHtml(pairs
14498
14818
  .filter(([, value]) => value !== '')
14499
14819
  .map(([prop, value]) => `${prop}:${value}`)
14500
- .join(';');
14820
+ .join(';'));
14501
14821
  }
14502
14822
  function attrString(styleValue, dir) {
14503
14823
  const stylePart = styleValue ? ` style="${styleValue}"` : '';
@@ -14604,17 +14924,17 @@ function registerPageFonts(reader, pageObj, registry, fonts) {
14604
14924
  try {
14605
14925
  const pageDict = reader.getDict(pageObj);
14606
14926
  const resources = pageDict['Resources'] ? reader.getDict(pageDict['Resources']) : {};
14607
- registerResourceFonts(reader, resources, registry, fonts, 0);
14927
+ registerResourceFonts(reader, resources, registry, fonts, { depth: 0, seen: new Set() });
14608
14928
  }
14609
14929
  catch {
14610
14930
  // A malformed resource dict must not abort the page's text extraction.
14611
14931
  }
14612
14932
  }
14613
- function registerResourceFonts(reader, resources, registry, fonts, depth) {
14614
- if (depth > MAX_RESOURCE_DEPTH)
14933
+ function registerResourceFonts(reader, resources, registry, fonts, walk) {
14934
+ if (walk.depth > MAX_RESOURCE_DEPTH || !visitOnce(walk.seen, resources))
14615
14935
  return;
14616
14936
  registerFontDict(reader, resources, registry, fonts);
14617
- descendIntoXObjects(reader, resources, registry, fonts, depth);
14937
+ descendIntoXObjects(reader, resources, registry, fonts, walk);
14618
14938
  }
14619
14939
  function registerFontDict(reader, resources, registry, fonts) {
14620
14940
  if (!resources['Font'])
@@ -14642,14 +14962,14 @@ function registerSingleFont(reader, name, ref, registry, fonts) {
14642
14962
  // A single broken font program must not abort the import.
14643
14963
  }
14644
14964
  }
14645
- function descendIntoXObjects(reader, resources, registry, fonts, depth) {
14965
+ function descendIntoXObjects(reader, resources, registry, fonts, walk) {
14646
14966
  if (!resources['XObject'])
14647
14967
  return;
14648
14968
  const xObjects = reader.getDict(resources['XObject']);
14649
14969
  for (const ref of Object.values(xObjects)) {
14650
14970
  const nested = resolveXObjectResources(reader, ref);
14651
14971
  if (nested)
14652
- registerResourceFonts(reader, nested, registry, fonts, depth + 1);
14972
+ registerResourceFonts(reader, nested, registry, fonts, { depth: walk.depth + 1, seen: walk.seen });
14653
14973
  }
14654
14974
  }
14655
14975
  function resolveXObjectResources(reader, ref) {
@@ -14876,10 +15196,11 @@ let importCounter = 0;
14876
15196
  * the original document (fonts, colors, alignment, spacing) without absolute
14877
15197
  * positioning. The returned `fontFaceCss` must be added to the host document
14878
15198
  * (e.g. a `<style>` in `<head>`) for embedded fonts to render.
15199
+ * Input is untrusted; structures exceeding internal limits are skipped.
14879
15200
  */
14880
- async function parsePdfReadable(buffer, options) {
15201
+ async function parsePdfReadable(buffer, options, limits = DEFAULT_PARSER_LIMITS) {
14881
15202
  const opts = normalizeOptions(options);
14882
- const reader = createValidatedReader(buffer);
15203
+ const reader = createValidatedReader(buffer, limits);
14883
15204
  const pages = reader.getPages();
14884
15205
  if (pages.length === 0) {
14885
15206
  throw new Error('PDF contains no pages.');
@@ -14912,7 +15233,7 @@ function normalizeOptions(options) {
14912
15233
  fontFamilyPrefix: options?.fontFamilyPrefix ?? `pdf${importCounter.toString(36)}-`,
14913
15234
  };
14914
15235
  }
14915
- function createValidatedReader(buffer) {
15236
+ function createValidatedReader(buffer, limits) {
14916
15237
  const header = new Uint8Array(buffer, 0, Math.min(5, buffer.byteLength));
14917
15238
  const isPdf = header.length === 5 &&
14918
15239
  header[0] === 0x25 && header[1] === 0x50 && header[2] === 0x44 &&
@@ -14920,7 +15241,7 @@ function createValidatedReader(buffer) {
14920
15241
  if (!isPdf) {
14921
15242
  throw new Error('Not a valid PDF file.');
14922
15243
  }
14923
- const reader = new PdfReader(buffer);
15244
+ const reader = new PdfReader(buffer, limits);
14924
15245
  try {
14925
15246
  reader.parse();
14926
15247
  }
@@ -15005,4 +15326,4 @@ function emitImageOnly(extracted, opts) {
15005
15326
  }
15006
15327
 
15007
15328
  export { parsePdfReadable };
15008
- //# sourceMappingURL=gilav21-shadcn-angular-rte-pdf-readable-DmE-CIxv.mjs.map
15329
+ //# sourceMappingURL=gilav21-shadcn-angular-rte-pdf-readable-JvCpuNQH.mjs.map