@changerawr/markdown 1.1.5 → 1.1.6

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,8 +1,10 @@
1
1
  // src/parser.ts
2
2
  var MarkdownParser = class {
3
+ // Cache compiled regexes
3
4
  constructor(config) {
4
5
  this.rules = [];
5
6
  this.warnings = [];
7
+ this.compiledPatterns = /* @__PURE__ */ new Map();
6
8
  this.config = {
7
9
  debugMode: false,
8
10
  maxIterations: 1e4,
@@ -12,6 +14,10 @@ var MarkdownParser = class {
12
14
  }
13
15
  addRule(rule) {
14
16
  this.rules.push(rule);
17
+ this.compiledPatterns.set(
18
+ rule,
19
+ new RegExp(rule.pattern.source, rule.pattern.flags.replace("g", ""))
20
+ );
15
21
  this.rules.sort((a, b) => {
16
22
  const aFeatureExtension = ["alert", "button", "embed"].includes(a.name);
17
23
  const bFeatureExtension = ["alert", "button", "embed"].includes(b.name);
@@ -57,12 +63,20 @@ var MarkdownParser = class {
57
63
  iterationCount++;
58
64
  let matched = false;
59
65
  let bestMatch = null;
66
+ let nextBestMatchIndex = null;
60
67
  for (const rule of this.rules) {
61
68
  try {
62
- const pattern = new RegExp(rule.pattern.source, rule.pattern.flags.replace("g", ""));
69
+ const pattern = this.compiledPatterns.get(rule);
63
70
  const match = remaining.match(pattern);
64
71
  if (match && match.index !== void 0) {
65
- const priority = match.index === 0 ? 1e3 : 1e3 - match.index;
72
+ if (match.index === 0) {
73
+ bestMatch = { rule, match, priority: 1e3 };
74
+ break;
75
+ }
76
+ if (nextBestMatchIndex === null || match.index < nextBestMatchIndex) {
77
+ nextBestMatchIndex = match.index;
78
+ }
79
+ const priority = 1e3 - match.index;
66
80
  if (!bestMatch || priority > bestMatch.priority || priority === bestMatch.priority && match.index < (bestMatch.match.index || 0)) {
67
81
  bestMatch = { rule, match, priority };
68
82
  }
@@ -109,15 +123,14 @@ var MarkdownParser = class {
109
123
  }
110
124
  }
111
125
  if (!matched) {
112
- const char = remaining[0];
113
- if (char) {
114
- tokens.push({
115
- type: "text",
116
- content: char,
117
- raw: char
118
- });
119
- remaining = remaining.slice(1);
120
- }
126
+ const chunkSize = nextBestMatchIndex !== null ? nextBestMatchIndex : Math.min(remaining.length, 1e3);
127
+ const textChunk = remaining.slice(0, chunkSize);
128
+ tokens.push({
129
+ type: "text",
130
+ content: textChunk,
131
+ raw: textChunk
132
+ });
133
+ remaining = remaining.slice(chunkSize);
121
134
  }
122
135
  }
123
136
  if (iterationCount >= maxIterations) {
@@ -535,6 +548,154 @@ var MarkdownRenderer = class {
535
548
  }
536
549
  };
537
550
 
551
+ // src/cache.ts
552
+ var LRUCache = class {
553
+ constructor(capacity = 100) {
554
+ this.cache = /* @__PURE__ */ new Map();
555
+ this.hits = 0;
556
+ this.misses = 0;
557
+ this.evictions = 0;
558
+ if (capacity <= 0) {
559
+ throw new Error("Cache capacity must be greater than 0");
560
+ }
561
+ this.capacity = capacity;
562
+ }
563
+ /**
564
+ * Get a value from the cache
565
+ */
566
+ get(key) {
567
+ const entry = this.cache.get(key);
568
+ if (entry) {
569
+ entry.timestamp = Date.now();
570
+ entry.accessCount++;
571
+ this.hits++;
572
+ this.cache.delete(key);
573
+ this.cache.set(key, entry);
574
+ return entry.value;
575
+ }
576
+ this.misses++;
577
+ return void 0;
578
+ }
579
+ /**
580
+ * Set a value in the cache
581
+ */
582
+ set(key, value) {
583
+ if (this.cache.has(key)) {
584
+ this.cache.delete(key);
585
+ } else if (this.cache.size >= this.capacity) {
586
+ this.evictLRU();
587
+ }
588
+ this.cache.set(key, {
589
+ value,
590
+ timestamp: Date.now(),
591
+ accessCount: 0
592
+ });
593
+ }
594
+ /**
595
+ * Check if a key exists in the cache
596
+ */
597
+ has(key) {
598
+ return this.cache.has(key);
599
+ }
600
+ /**
601
+ * Delete a specific key from the cache
602
+ */
603
+ delete(key) {
604
+ return this.cache.delete(key);
605
+ }
606
+ /**
607
+ * Clear all entries from the cache
608
+ */
609
+ clear() {
610
+ this.cache.clear();
611
+ this.hits = 0;
612
+ this.misses = 0;
613
+ this.evictions = 0;
614
+ }
615
+ /**
616
+ * Get the current size of the cache
617
+ */
618
+ get size() {
619
+ return this.cache.size;
620
+ }
621
+ /**
622
+ * Get cache statistics
623
+ */
624
+ getStats() {
625
+ const totalRequests = this.hits + this.misses;
626
+ return {
627
+ size: this.cache.size,
628
+ capacity: this.capacity,
629
+ hits: this.hits,
630
+ misses: this.misses,
631
+ hitRate: totalRequests > 0 ? this.hits / totalRequests : 0,
632
+ evictions: this.evictions
633
+ };
634
+ }
635
+ /**
636
+ * Reset cache statistics
637
+ */
638
+ resetStats() {
639
+ this.hits = 0;
640
+ this.misses = 0;
641
+ this.evictions = 0;
642
+ }
643
+ /**
644
+ * Get all keys in the cache
645
+ */
646
+ keys() {
647
+ return Array.from(this.cache.keys());
648
+ }
649
+ /**
650
+ * Get all values in the cache
651
+ */
652
+ values() {
653
+ return Array.from(this.cache.values()).map((entry) => entry.value);
654
+ }
655
+ /**
656
+ * Update cache capacity and evict if necessary
657
+ */
658
+ setCapacity(newCapacity) {
659
+ if (newCapacity <= 0) {
660
+ throw new Error("Cache capacity must be greater than 0");
661
+ }
662
+ this.capacity = newCapacity;
663
+ while (this.cache.size > this.capacity) {
664
+ this.evictLRU();
665
+ }
666
+ }
667
+ /**
668
+ * Evict the least recently used entry
669
+ */
670
+ evictLRU() {
671
+ const firstKey = this.cache.keys().next().value;
672
+ if (firstKey !== void 0) {
673
+ this.cache.delete(firstKey);
674
+ this.evictions++;
675
+ }
676
+ }
677
+ };
678
+ function hashContent(content) {
679
+ if (content.length > 1e4) {
680
+ const start = content.slice(0, 1e3);
681
+ const middle = content.slice(Math.floor(content.length / 2) - 500, Math.floor(content.length / 2) + 500);
682
+ const end = content.slice(-1e3);
683
+ const sample = content.length + "|" + start + middle + end;
684
+ let hash2 = 2166136261;
685
+ for (let i = 0; i < sample.length; i++) {
686
+ hash2 ^= sample.charCodeAt(i);
687
+ hash2 += (hash2 << 1) + (hash2 << 4) + (hash2 << 7) + (hash2 << 8) + (hash2 << 24);
688
+ }
689
+ return (hash2 >>> 0).toString(36);
690
+ }
691
+ let hash = 2166136261;
692
+ for (let i = 0; i < content.length; i++) {
693
+ hash ^= content.charCodeAt(i);
694
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
695
+ }
696
+ return (hash >>> 0).toString(36);
697
+ }
698
+
538
699
  // src/extensions/core/blockquote.ts
539
700
  var BlockquoteExtension = {
540
701
  name: "blockquote",
@@ -1507,8 +1668,13 @@ function extractVimeoId(url) {
1507
1668
  var ChangerawrMarkdown = class {
1508
1669
  constructor(config) {
1509
1670
  this.extensions = /* @__PURE__ */ new Map();
1671
+ this.parseTime = 0;
1672
+ this.renderTime = 0;
1673
+ this.lastTokenCount = 0;
1510
1674
  this.parser = new MarkdownParser(config?.parser);
1511
1675
  this.renderer = new MarkdownRenderer(config?.renderer);
1676
+ this.parseCache = new LRUCache(100);
1677
+ this.renderCache = new LRUCache(100);
1512
1678
  this.registerCoreExtensions();
1513
1679
  this.registerFeatureExtensions();
1514
1680
  if (config?.extensions) {
@@ -1591,14 +1757,83 @@ var ChangerawrMarkdown = class {
1591
1757
  }
1592
1758
  }
1593
1759
  parse(markdown2) {
1594
- return this.parser.parse(markdown2);
1760
+ const cacheKey = hashContent(markdown2);
1761
+ const cached = this.parseCache.get(cacheKey);
1762
+ if (cached) {
1763
+ this.lastTokenCount = cached.length;
1764
+ return cached;
1765
+ }
1766
+ const startTime = performance.now();
1767
+ const tokens = this.parser.parse(markdown2);
1768
+ this.parseTime = performance.now() - startTime;
1769
+ this.lastTokenCount = tokens.length;
1770
+ this.parseCache.set(cacheKey, tokens);
1771
+ return tokens;
1595
1772
  }
1596
- render(tokens) {
1597
- return this.renderer.render(tokens);
1773
+ render(tokens, cacheKey) {
1774
+ if (cacheKey) {
1775
+ const cached = this.renderCache.get(cacheKey);
1776
+ if (cached) {
1777
+ return cached;
1778
+ }
1779
+ }
1780
+ const startTime = performance.now();
1781
+ const html = this.renderer.render(tokens);
1782
+ this.renderTime = performance.now() - startTime;
1783
+ if (cacheKey) {
1784
+ this.renderCache.set(cacheKey, html);
1785
+ }
1786
+ return html;
1598
1787
  }
1599
1788
  toHtml(markdown2) {
1789
+ const cacheKey = hashContent(markdown2);
1790
+ const cachedHtml = this.renderCache.get(cacheKey);
1791
+ if (cachedHtml) {
1792
+ return cachedHtml;
1793
+ }
1600
1794
  const tokens = this.parse(markdown2);
1601
- return this.render(tokens);
1795
+ return this.render(tokens, cacheKey);
1796
+ }
1797
+ /**
1798
+ * Render markdown with performance metrics
1799
+ */
1800
+ toHtmlWithMetrics(markdown2) {
1801
+ const startTotal = performance.now();
1802
+ const parseCacheKey = hashContent(markdown2);
1803
+ const parseCacheHit = this.parseCache.has(parseCacheKey);
1804
+ const html = this.toHtml(markdown2);
1805
+ const totalTime = performance.now() - startTotal;
1806
+ const metrics = {
1807
+ inputSize: markdown2.length,
1808
+ parseTime: this.parseTime,
1809
+ renderTime: this.renderTime,
1810
+ totalTime,
1811
+ tokenCount: this.lastTokenCount,
1812
+ cacheHit: parseCacheHit
1813
+ };
1814
+ return { html, metrics };
1815
+ }
1816
+ /**
1817
+ * Stream-render large documents in chunks for better performance
1818
+ */
1819
+ async toHtmlStreamed(markdown2, options = {}) {
1820
+ const chunkSize = options.chunkSize || 50;
1821
+ const tokens = this.parse(markdown2);
1822
+ const totalTokens = tokens.length;
1823
+ const chunks = [];
1824
+ for (let i = 0; i < tokens.length; i += chunkSize) {
1825
+ const chunkTokens = tokens.slice(i, Math.min(i + chunkSize, tokens.length));
1826
+ const chunkHtml = this.render(chunkTokens);
1827
+ chunks.push(chunkHtml);
1828
+ if (options.onChunk) {
1829
+ options.onChunk({
1830
+ html: chunkHtml,
1831
+ progress: Math.min(i + chunkSize, tokens.length) / totalTokens
1832
+ });
1833
+ }
1834
+ await new Promise((resolve) => setTimeout(resolve, 0));
1835
+ }
1836
+ return chunks.join("");
1602
1837
  }
1603
1838
  getExtensions() {
1604
1839
  return Array.from(this.extensions.keys());
@@ -1612,25 +1847,50 @@ var ChangerawrMarkdown = class {
1612
1847
  getDebugInfo() {
1613
1848
  return {
1614
1849
  warnings: this.getWarnings(),
1615
- parseTime: 0,
1616
- renderTime: 0,
1617
- tokenCount: 0,
1850
+ parseTime: this.parseTime,
1851
+ renderTime: this.renderTime,
1852
+ tokenCount: this.lastTokenCount,
1618
1853
  iterationCount: 0
1619
1854
  };
1620
1855
  }
1621
1856
  getPerformanceMetrics() {
1622
1857
  return {
1623
- parseTime: 0,
1624
- renderTime: 0,
1625
- totalTime: 0,
1626
- tokenCount: 0
1858
+ parseTime: this.parseTime,
1859
+ renderTime: this.renderTime,
1860
+ totalTime: this.parseTime + this.renderTime,
1861
+ tokenCount: this.lastTokenCount
1862
+ };
1863
+ }
1864
+ /**
1865
+ * Get cache statistics
1866
+ */
1867
+ getCacheStats() {
1868
+ return {
1869
+ parse: this.parseCache.getStats(),
1870
+ render: this.renderCache.getStats()
1627
1871
  };
1628
1872
  }
1873
+ /**
1874
+ * Clear all caches
1875
+ */
1876
+ clearCaches() {
1877
+ this.parseCache.clear();
1878
+ this.renderCache.clear();
1879
+ }
1880
+ /**
1881
+ * Update cache capacity
1882
+ */
1883
+ setCacheSize(size) {
1884
+ this.parseCache.setCapacity(size);
1885
+ this.renderCache.setCapacity(size);
1886
+ }
1629
1887
  rebuildParserAndRenderer() {
1630
1888
  const parserConfig = this.parser.getConfig();
1631
1889
  const rendererConfig = this.renderer.getConfig();
1632
1890
  this.parser = new MarkdownParser(parserConfig);
1633
1891
  this.renderer = new MarkdownRenderer(rendererConfig);
1892
+ this.parseCache.clear();
1893
+ this.renderCache.clear();
1634
1894
  const extensionsToRegister = Array.from(this.extensions.values());
1635
1895
  const featureExtensions = extensionsToRegister.filter(
1636
1896
  (ext) => ["alert", "button", "embed"].includes(ext.name)