docrev 0.10.2 → 0.11.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.
Files changed (87) hide show
  1. package/dist/lib/anchor-match.d.ts +3 -1
  2. package/dist/lib/anchor-match.d.ts.map +1 -1
  3. package/dist/lib/anchor-match.js +5 -7
  4. package/dist/lib/anchor-match.js.map +1 -1
  5. package/dist/lib/annotations.d.ts +0 -6
  6. package/dist/lib/annotations.d.ts.map +1 -1
  7. package/dist/lib/annotations.js +26 -8
  8. package/dist/lib/annotations.js.map +1 -1
  9. package/dist/lib/build.d.ts.map +1 -1
  10. package/dist/lib/build.js +9 -6
  11. package/dist/lib/build.js.map +1 -1
  12. package/dist/lib/commands/build.d.ts.map +1 -1
  13. package/dist/lib/commands/build.js +5 -0
  14. package/dist/lib/commands/build.js.map +1 -1
  15. package/dist/lib/commands/doi.d.ts.map +1 -1
  16. package/dist/lib/commands/doi.js +14 -2
  17. package/dist/lib/commands/doi.js.map +1 -1
  18. package/dist/lib/commands/sync.d.ts.map +1 -1
  19. package/dist/lib/commands/sync.js +24 -11
  20. package/dist/lib/commands/sync.js.map +1 -1
  21. package/dist/lib/comment-realign.d.ts +12 -21
  22. package/dist/lib/comment-realign.d.ts.map +1 -1
  23. package/dist/lib/comment-realign.js +40 -348
  24. package/dist/lib/comment-realign.js.map +1 -1
  25. package/dist/lib/dependencies.d.ts +10 -0
  26. package/dist/lib/dependencies.d.ts.map +1 -1
  27. package/dist/lib/dependencies.js +41 -9
  28. package/dist/lib/dependencies.js.map +1 -1
  29. package/dist/lib/doi.d.ts.map +1 -1
  30. package/dist/lib/doi.js +68 -80
  31. package/dist/lib/doi.js.map +1 -1
  32. package/dist/lib/errors.d.ts.map +1 -1
  33. package/dist/lib/errors.js +3 -1
  34. package/dist/lib/errors.js.map +1 -1
  35. package/dist/lib/import.d.ts +7 -4
  36. package/dist/lib/import.d.ts.map +1 -1
  37. package/dist/lib/import.js +58 -62
  38. package/dist/lib/import.js.map +1 -1
  39. package/dist/lib/ooxml.d.ts +207 -0
  40. package/dist/lib/ooxml.d.ts.map +1 -0
  41. package/dist/lib/ooxml.js +634 -0
  42. package/dist/lib/ooxml.js.map +1 -0
  43. package/dist/lib/rate-limiter.d.ts +8 -0
  44. package/dist/lib/rate-limiter.d.ts.map +1 -1
  45. package/dist/lib/rate-limiter.js +41 -4
  46. package/dist/lib/rate-limiter.js.map +1 -1
  47. package/dist/lib/types.d.ts +9 -1
  48. package/dist/lib/types.d.ts.map +1 -1
  49. package/dist/lib/word-extraction.d.ts.map +1 -1
  50. package/dist/lib/word-extraction.js +48 -307
  51. package/dist/lib/word-extraction.js.map +1 -1
  52. package/dist/lib/word.d.ts.map +1 -1
  53. package/dist/lib/word.js +80 -175
  54. package/dist/lib/word.js.map +1 -1
  55. package/dist/lib/wordcomments.d.ts.map +1 -1
  56. package/dist/lib/wordcomments.js +53 -92
  57. package/dist/lib/wordcomments.js.map +1 -1
  58. package/docs-src/build.py +113 -113
  59. package/docs-src/extra.css +208 -208
  60. package/docs-src/md-to-html.lua +6 -6
  61. package/docs-src/template.html +116 -116
  62. package/lib/anchor-match.ts +6 -7
  63. package/lib/annotations.ts +28 -8
  64. package/lib/build.ts +10 -6
  65. package/lib/commands/build.ts +5 -0
  66. package/lib/commands/doi.ts +17 -1
  67. package/lib/commands/sync.ts +26 -13
  68. package/lib/comment-realign.ts +46 -464
  69. package/lib/dependencies.ts +43 -9
  70. package/lib/doi.ts +76 -94
  71. package/lib/errors.ts +5 -1
  72. package/lib/import.ts +65 -70
  73. package/lib/ooxml.ts +768 -0
  74. package/lib/rate-limiter.ts +37 -4
  75. package/lib/types.ts +9 -1
  76. package/lib/word-extraction.ts +49 -330
  77. package/lib/word.ts +86 -203
  78. package/lib/wordcomments.ts +53 -104
  79. package/mkdocs.yml +64 -64
  80. package/package.json +1 -1
  81. package/issues.md +0 -180
  82. package/site/assets/extra.css +0 -208
  83. package/site/commands.html +0 -926
  84. package/site/configuration.html +0 -469
  85. package/site/index.html +0 -288
  86. package/site/troubleshooting.html +0 -461
  87. package/site/workflow.html +0 -518
@@ -1,116 +1,116 @@
1
- <!DOCTYPE html>
2
- <html lang="en" data-bs-theme="light">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>$if(pagetitle)$$pagetitle$ — $endif$docrev</title>
7
- <meta name="description" content="CLI for writing documents in Markdown while collaborating with Word users.">
8
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/sandstone/bootstrap.min.css">
9
- $if(highlighting-css)$
10
- <style>
11
- $highlighting-css$
12
- </style>
13
- $endif$
14
- <link rel="stylesheet" href="assets/extra.css">
15
- </head>
16
- <body>
17
-
18
- <nav class="navbar navbar-expand-lg fixed-top" aria-label="Site navigation">
19
- <div class="container">
20
- <a class="navbar-brand me-2" href="index.html">docrev</a>
21
- <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-main" aria-controls="navbar-main" aria-expanded="false" aria-label="Toggle navigation">
22
- <span class="navbar-toggler-icon"></span>
23
- </button>
24
- <div class="collapse navbar-collapse" id="navbar-main">
25
- <ul class="navbar-nav me-auto">
26
- <li class="nav-item">
27
- <a class="nav-link$if(active-home)$ active$endif$" $if(active-home)$aria-current="page"$endif$ href="index.html">Home</a>
28
- </li>
29
- <li class="nav-item">
30
- <a class="nav-link$if(active-workflow)$ active$endif$" $if(active-workflow)$aria-current="page"$endif$ href="workflow.html">Get Started</a>
31
- </li>
32
- <li class="nav-item">
33
- <a class="nav-link$if(active-commands)$ active$endif$" $if(active-commands)$aria-current="page"$endif$ href="commands.html">Commands</a>
34
- </li>
35
- <li class="nav-item">
36
- <a class="nav-link$if(active-configuration)$ active$endif$" $if(active-configuration)$aria-current="page"$endif$ href="configuration.html">Configuration</a>
37
- </li>
38
- <li class="nav-item">
39
- <a class="nav-link$if(active-troubleshooting)$ active$endif$" $if(active-troubleshooting)$aria-current="page"$endif$ href="troubleshooting.html">Troubleshooting</a>
40
- </li>
41
- </ul>
42
- <ul class="navbar-nav">
43
- <li class="nav-item">
44
- <button id="theme-toggle" class="btn btn-link nav-link border-0" aria-label="Toggle dark mode">
45
- <svg id="icon-sun" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
46
- <path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8M8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0m0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13m8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5M3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8m10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0m-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0m9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707M4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707"/>
47
- </svg>
48
- <svg id="icon-moon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="display:none">
49
- <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278"/>
50
- </svg>
51
- </button>
52
- </li>
53
- <li class="nav-item">
54
- <a class="nav-link" href="https://github.com/gcol33/docrev" aria-label="GitHub">
55
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
56
- <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"/>
57
- </svg>
58
- </a>
59
- </li>
60
- <li class="nav-item">
61
- <a class="nav-link" href="https://www.npmjs.com/package/docrev" aria-label="npm">
62
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
63
- <path d="M0 0v16h16V0zm5.333 13.333H2.667V5.333h2.666v5.334H8V5.333h2.667v8H5.333z"/>
64
- </svg>
65
- </a>
66
- </li>
67
- </ul>
68
- </div>
69
- </div>
70
- </nav>
71
-
72
- <div class="container template-$if(active-home)$home$else$article$endif$">
73
- <div class="row">
74
- <main id="main" class="$if(toc)$col-md-9$else$col-12$endif$">
75
- $body$
76
- </main>
77
- $if(toc)$
78
- <aside class="col-md-3 d-none d-md-block">
79
- <nav id="toc" data-toggle="toc" aria-label="On this page">
80
- <h2 class="h6">On this page</h2>
81
- <div class="toc">$toc$</div>
82
- </nav>
83
- </aside>
84
- $endif$
85
- </div>
86
- </div>
87
-
88
- <footer class="border-top py-3 mt-auto">
89
- <div class="container d-flex justify-content-between align-items-center text-muted" style="font-size:.85rem">
90
- <span>docrev$if(version)$ $version$$endif$</span>
91
- <span>Built with <a href="https://pandoc.org" class="text-muted">pandoc</a></span>
92
- </div>
93
- </footer>
94
-
95
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
96
- <script>
97
- (function () {
98
- var html = document.documentElement;
99
- var sun = document.getElementById('icon-sun');
100
- var moon = document.getElementById('icon-moon');
101
- function apply(t) {
102
- html.setAttribute('data-bs-theme', t);
103
- sun.style.display = t === 'dark' ? 'none' : '';
104
- moon.style.display = t === 'dark' ? '' : 'none';
105
- }
106
- apply(localStorage.getItem('docrev-theme') || 'light');
107
- document.getElementById('theme-toggle').addEventListener('click', function () {
108
- var next = html.getAttribute('data-bs-theme') === 'dark' ? 'light' : 'dark';
109
- localStorage.setItem('docrev-theme', next);
110
- apply(next);
111
- });
112
- })();
113
- </script>
114
-
115
- </body>
116
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-bs-theme="light">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>$if(pagetitle)$$pagetitle$ — $endif$docrev</title>
7
+ <meta name="description" content="CLI for writing documents in Markdown while collaborating with Word users.">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/sandstone/bootstrap.min.css">
9
+ $if(highlighting-css)$
10
+ <style>
11
+ $highlighting-css$
12
+ </style>
13
+ $endif$
14
+ <link rel="stylesheet" href="assets/extra.css">
15
+ </head>
16
+ <body>
17
+
18
+ <nav class="navbar navbar-expand-lg fixed-top" aria-label="Site navigation">
19
+ <div class="container">
20
+ <a class="navbar-brand me-2" href="index.html">docrev</a>
21
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-main" aria-controls="navbar-main" aria-expanded="false" aria-label="Toggle navigation">
22
+ <span class="navbar-toggler-icon"></span>
23
+ </button>
24
+ <div class="collapse navbar-collapse" id="navbar-main">
25
+ <ul class="navbar-nav me-auto">
26
+ <li class="nav-item">
27
+ <a class="nav-link$if(active-home)$ active$endif$" $if(active-home)$aria-current="page"$endif$ href="index.html">Home</a>
28
+ </li>
29
+ <li class="nav-item">
30
+ <a class="nav-link$if(active-workflow)$ active$endif$" $if(active-workflow)$aria-current="page"$endif$ href="workflow.html">Get Started</a>
31
+ </li>
32
+ <li class="nav-item">
33
+ <a class="nav-link$if(active-commands)$ active$endif$" $if(active-commands)$aria-current="page"$endif$ href="commands.html">Commands</a>
34
+ </li>
35
+ <li class="nav-item">
36
+ <a class="nav-link$if(active-configuration)$ active$endif$" $if(active-configuration)$aria-current="page"$endif$ href="configuration.html">Configuration</a>
37
+ </li>
38
+ <li class="nav-item">
39
+ <a class="nav-link$if(active-troubleshooting)$ active$endif$" $if(active-troubleshooting)$aria-current="page"$endif$ href="troubleshooting.html">Troubleshooting</a>
40
+ </li>
41
+ </ul>
42
+ <ul class="navbar-nav">
43
+ <li class="nav-item">
44
+ <button id="theme-toggle" class="btn btn-link nav-link border-0" aria-label="Toggle dark mode">
45
+ <svg id="icon-sun" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
46
+ <path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8M8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0m0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13m8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5M3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8m10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0m-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0m9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707M4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707"/>
47
+ </svg>
48
+ <svg id="icon-moon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="display:none">
49
+ <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278"/>
50
+ </svg>
51
+ </button>
52
+ </li>
53
+ <li class="nav-item">
54
+ <a class="nav-link" href="https://github.com/gcol33/docrev" aria-label="GitHub">
55
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
56
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8"/>
57
+ </svg>
58
+ </a>
59
+ </li>
60
+ <li class="nav-item">
61
+ <a class="nav-link" href="https://www.npmjs.com/package/docrev" aria-label="npm">
62
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
63
+ <path d="M0 0v16h16V0zm5.333 13.333H2.667V5.333h2.666v5.334H8V5.333h2.667v8H5.333z"/>
64
+ </svg>
65
+ </a>
66
+ </li>
67
+ </ul>
68
+ </div>
69
+ </div>
70
+ </nav>
71
+
72
+ <div class="container template-$if(active-home)$home$else$article$endif$">
73
+ <div class="row">
74
+ <main id="main" class="$if(toc)$col-md-9$else$col-12$endif$">
75
+ $body$
76
+ </main>
77
+ $if(toc)$
78
+ <aside class="col-md-3 d-none d-md-block">
79
+ <nav id="toc" data-toggle="toc" aria-label="On this page">
80
+ <h2 class="h6">On this page</h2>
81
+ <div class="toc">$toc$</div>
82
+ </nav>
83
+ </aside>
84
+ $endif$
85
+ </div>
86
+ </div>
87
+
88
+ <footer class="border-top py-3 mt-auto">
89
+ <div class="container d-flex justify-content-between align-items-center text-muted" style="font-size:.85rem">
90
+ <span>docrev$if(version)$ $version$$endif$</span>
91
+ <span>Built with <a href="https://pandoc.org" class="text-muted">pandoc</a></span>
92
+ </div>
93
+ </footer>
94
+
95
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
96
+ <script>
97
+ (function () {
98
+ var html = document.documentElement;
99
+ var sun = document.getElementById('icon-sun');
100
+ var moon = document.getElementById('icon-moon');
101
+ function apply(t) {
102
+ html.setAttribute('data-bs-theme', t);
103
+ sun.style.display = t === 'dark' ? 'none' : '';
104
+ moon.style.display = t === 'dark' ? '' : 'none';
105
+ }
106
+ apply(localStorage.getItem('docrev-theme') || 'light');
107
+ document.getElementById('theme-toggle').addEventListener('click', function () {
108
+ var next = html.getAttribute('data-bs-theme') === 'dark' ? 'light' : 'dark';
109
+ localStorage.setItem('docrev-theme', next);
110
+ apply(next);
111
+ });
112
+ })();
113
+ </script>
114
+
115
+ </body>
116
+ </html>
@@ -5,6 +5,8 @@
5
5
  * a target text using progressively looser strategies.
6
6
  */
7
7
 
8
+ import { stripAnnotations } from './annotations.js';
9
+
8
10
  export type AnchorStrategy =
9
11
  | 'direct'
10
12
  | 'normalized'
@@ -30,15 +32,12 @@ export interface AnchorSearchResult {
30
32
  /**
31
33
  * Strip CriticMarkup so the matcher sees plain prose instead of
32
34
  * `{++inserted++}`/`{--deleted--}`/etc. Used when an anchor lives
33
- * underneath previously imported track changes.
35
+ * underneath previously imported track changes. Delegates to the single
36
+ * annotation stripper so the matcher and the build path resolve markup the
37
+ * same way — a payload containing `+`, `-`, or `~` is handled correctly.
34
38
  */
35
39
  export function stripCriticMarkup(text: string): string {
36
- return text
37
- .replace(/\{\+\+([^+]*)\+\+\}/g, '$1') // insertions: keep new text
38
- .replace(/\{--([^-]*)--\}/g, '') // deletions: remove old text
39
- .replace(/\{~~([^~]*)~>([^~]*)~~\}/g, '$2') // substitutions: keep new text
40
- .replace(/\{>>[\s\S]*?<<\}/g, '') // comments: remove (non-greedy; comment text may contain '<')
41
- .replace(/\[([^\]]*)\]\{\.mark\}/g, '$1'); // marked text: keep text
40
+ return stripAnnotations(text);
42
41
  }
43
42
 
44
43
  /**
@@ -412,16 +412,36 @@ export function stripToSingleSpace(text: string): string {
412
412
  * @returns True if text contains any annotations
413
413
  * @throws TypeError If text is not a string
414
414
  */
415
+ // Non-global copies for membership tests. `.test()` on a global regex advances
416
+ // its lastIndex, so reusing PATTERNS here would make the result depend on prior
417
+ // calls and intermittently miss real annotations.
418
+ const ANNOTATION_TESTERS = [
419
+ PATTERNS.insert,
420
+ PATTERNS.delete,
421
+ PATTERNS.substitute,
422
+ PATTERNS.comment,
423
+ PATTERNS.highlight,
424
+ ].map((re) => new RegExp(re.source, re.flags.replace('g', '')));
425
+
415
426
  export function hasAnnotations(text: string): boolean {
416
427
  if (typeof text !== 'string') {
417
428
  throw new TypeError(`text must be a string, got ${typeof text}`);
418
429
  }
419
430
 
420
- return PATTERNS.insert.test(text) ||
421
- PATTERNS.delete.test(text) ||
422
- PATTERNS.substitute.test(text) ||
423
- PATTERNS.comment.test(text) ||
424
- PATTERNS.highlight.test(text);
431
+ return ANNOTATION_TESTERS.some((re) => re.test(text));
432
+ }
433
+
434
+ /**
435
+ * Replace the specific annotation occurrence at `position` rather than the
436
+ * first textual match. Two identical annotations (e.g. the same `{++the++}`
437
+ * twice) would otherwise have the wrong one edited. Falls back to a
438
+ * first-occurrence replace only if the recorded position no longer lines up.
439
+ */
440
+ function replaceAnnotationAt(text: string, match: string, position: number, replacement: string): string {
441
+ if (position >= 0 && text.startsWith(match, position)) {
442
+ return text.slice(0, position) + replacement + text.slice(position + match.length);
443
+ }
444
+ return text.replace(match, replacement);
425
445
  }
426
446
 
427
447
  /**
@@ -480,7 +500,7 @@ export function applyDecision(text: string, annotation: Annotation, accept: bool
480
500
  return text;
481
501
  }
482
502
 
483
- return text.replace(annotation.match, replacement);
503
+ return replaceAnnotationAt(text, annotation.match, annotation.position, replacement);
484
504
  }
485
505
 
486
506
  /**
@@ -549,11 +569,11 @@ export function setCommentStatus(text: string, comment: Annotation, resolved: bo
549
569
  if (resolved) {
550
570
  // Add [RESOLVED] marker before the closing <<
551
571
  const newMatch = originalMatch.replace(/<<\}$/, ' [RESOLVED]<<}');
552
- return text.replace(originalMatch, newMatch);
572
+ return replaceAnnotationAt(text, originalMatch, comment.position, newMatch);
553
573
  } else {
554
574
  // Remove resolved markers
555
575
  const newMatch = originalMatch.replace(/\s*\[(RESOLVED|✓)\]<<\}$/, '<<}');
556
- return text.replace(originalMatch, newMatch);
576
+ return replaceAnnotationAt(text, originalMatch, comment.position, newMatch);
557
577
  }
558
578
  }
559
579
 
package/lib/build.ts CHANGED
@@ -1615,20 +1615,24 @@ export async function runPandoc(
1615
1615
  pandoc.on('close', async (code) => {
1616
1616
  cleanupMacroTempFiles();
1617
1617
  if (code === 0) {
1618
- // For PPTX, post-process to add slide numbers, buildup colors, and logos
1618
+ // For PPTX, post-process to add slide numbers, buildup colors, and logos.
1619
+ // These steps enhance an already-written deck; a failure degrades the
1620
+ // output rather than aborting it, so each is reported as a warning
1621
+ // instead of silently swallowed — otherwise the user gets a deck
1622
+ // missing slide numbers / colors / logos with no indication why.
1619
1623
  if (format === 'pptx') {
1624
+ const warn = (step: string, e: unknown) =>
1625
+ console.warn(`Warning: PPTX ${step} failed; deck written without it (${(e as Error).message})`);
1620
1626
  try {
1621
- // Inject slide numbers into content slides only
1622
1627
  await injectSlideNumbers(outputPath);
1623
1628
  } catch (e) {
1624
- // Slide number injection failed but output was created
1629
+ warn('slide numbering', e);
1625
1630
  }
1626
1631
  try {
1627
1632
  // Apply colors (default text color, title color, buildup greying)
1628
1633
  const pptxConfig = config.pptx || {};
1629
1634
  const colorsConfig = pptxConfig.colors || {};
1630
1635
  const buildupConfig = pptxConfig.buildup || {};
1631
- // Merge colors and buildup config for applyBuildupColors
1632
1636
  const colorConfig = {
1633
1637
  default: colorsConfig.default,
1634
1638
  title: colorsConfig.title,
@@ -1638,14 +1642,14 @@ export async function runPandoc(
1638
1642
  };
1639
1643
  await applyBuildupColors(outputPath, colorConfig);
1640
1644
  } catch (e) {
1641
- // Color application failed but output was created
1645
+ warn('color application', e);
1642
1646
  }
1643
1647
  // Inject logos into cover slide (if media dir configured)
1644
1648
  if (pptxMediaDir) {
1645
1649
  try {
1646
1650
  await injectMediaIntoPptx(outputPath, pptxMediaDir);
1647
1651
  } catch (e) {
1648
- // Logo injection failed but output was created
1652
+ warn('logo injection', e);
1649
1653
  }
1650
1654
  }
1651
1655
  }
@@ -25,6 +25,7 @@ import {
25
25
  } from './context.js';
26
26
  import type { Command } from 'commander';
27
27
  import * as readline from 'readline';
28
+ import { getBuildSuggestions } from '../errors.js';
28
29
 
29
30
  interface RefsOptions {
30
31
  dir: string;
@@ -673,6 +674,10 @@ export function register(program: Command, pkg?: { version?: string }): void {
673
674
  console.log('');
674
675
  for (const f of failed) {
675
676
  console.error(chalk.red(`\n${f.format} error:\n${f.error}`));
677
+ const issue = f.format === 'pdf' || f.format === 'beamer' ? 'latex_error' : 'pandoc_failed';
678
+ for (const suggestion of getBuildSuggestions(issue, { format: f.format })) {
679
+ console.error(chalk.dim(` ${suggestion}`));
680
+ }
676
681
  }
677
682
  process.exit(1);
678
683
  }
@@ -67,6 +67,7 @@ export function register(program: Command): void {
67
67
  // Group results by status
68
68
  const valid = results.entries.filter(e => e.status === 'valid');
69
69
  const invalid = results.entries.filter(e => e.status === 'invalid');
70
+ const unreachable = results.entries.filter(e => e.status === 'unreachable');
70
71
  const missing = results.entries.filter(e => e.status === 'missing');
71
72
  const skipped = results.entries.filter(e => e.status === 'skipped');
72
73
 
@@ -74,9 +75,14 @@ export function register(program: Command): void {
74
75
  const summaryRows: string[][] = [
75
76
  [chalk.green('Valid'), chalk.green(valid.length.toString())],
76
77
  [invalid.length > 0 ? chalk.red('Invalid') : 'Invalid', invalid.length > 0 ? chalk.red(invalid.length.toString()) : '0'],
78
+ ];
79
+ if (unreachable.length > 0) {
80
+ summaryRows.push([chalk.yellow('Unreachable'), chalk.yellow(unreachable.length.toString())]);
81
+ }
82
+ summaryRows.push(
77
83
  [missing.length > 0 ? chalk.yellow('Missing (articles)') : 'Missing', missing.length > 0 ? chalk.yellow(missing.length.toString()) : '0'],
78
84
  [chalk.dim('Skipped'), chalk.dim(skipped.length.toString())],
79
- ];
85
+ );
80
86
  console.log(fmt.table(['Status', 'Count'], summaryRows));
81
87
  console.log();
82
88
 
@@ -90,6 +96,16 @@ export function register(program: Command): void {
90
96
  console.log();
91
97
  }
92
98
 
99
+ // Show unreachable DOIs (network issue — not necessarily invalid)
100
+ if (unreachable.length > 0) {
101
+ console.log(chalk.yellow('Unreachable (could not contact registry — DOI may be fine):'));
102
+ for (const e of unreachable) {
103
+ console.log(` ${chalk.bold(e.key)}: ${e.doi || 'N/A'}`);
104
+ console.log(chalk.dim(` ${e.message}`));
105
+ }
106
+ console.log();
107
+ }
108
+
93
109
  // Show missing (articles without DOI)
94
110
  if (missing.length > 0) {
95
111
  console.log(chalk.yellow('Missing DOIs (should have DOI):'));
@@ -305,6 +305,7 @@ export function register(program: Command): void {
305
305
  // reports placements rather than the raw extracted count.
306
306
  const routedCommentIds = new Set<string>();
307
307
  let totalCommentsPlaced = 0;
308
+ let totalCommentsLowConfidence = 0;
308
309
  let totalCommentsDeduped = 0;
309
310
  let totalCommentsUnmatched = 0;
310
311
 
@@ -375,19 +376,23 @@ export function register(program: Command): void {
375
376
 
376
377
  if (sectionComments.length > 0) {
377
378
  for (const c of sectionComments) routedCommentIds.add(c.id);
378
- const cstats = { placed: 0, deduped: 0, unmatched: 0 };
379
+ const cstats = { placed: 0, lowConfidence: 0, deduped: 0, unmatched: 0 };
379
380
  annotated = insertCommentsIntoMarkdown(annotated, sectionComments, anchors, {
380
381
  quiet: !process.env.DEBUG,
381
382
  sectionBoundary: boundary,
382
383
  outStats: cstats,
383
384
  });
384
- stats.comments = (stats.comments || 0) + cstats.placed;
385
- totalCommentsPlaced += cstats.placed;
385
+ // Both confident and approximate placements are written to the
386
+ // file; count both as placed, but track the approximate ones so
387
+ // the summary can flag them for review.
388
+ stats.comments = (stats.comments || 0) + cstats.placed + cstats.lowConfidence;
389
+ totalCommentsPlaced += cstats.placed + cstats.lowConfidence;
390
+ totalCommentsLowConfidence += cstats.lowConfidence;
386
391
  totalCommentsDeduped += cstats.deduped;
387
392
  totalCommentsUnmatched += cstats.unmatched;
388
393
 
389
394
  if (process.env.DEBUG) {
390
- console.log(`[DEBUG] ${section.file}: placed ${cstats.placed}, deduped ${cstats.deduped}, unmatched ${cstats.unmatched} of ${sectionComments.length}`);
395
+ console.log(`[DEBUG] ${section.file}: placed ${cstats.placed}, approx ${cstats.lowConfidence}, deduped ${cstats.deduped}, unmatched ${cstats.unmatched} of ${sectionComments.length}`);
391
396
  }
392
397
  }
393
398
  }
@@ -485,6 +490,9 @@ export function register(program: Command): void {
485
490
  if (totalCommentsPlaced > 0) {
486
491
  summaryLines.push(`${chalk.bold(totalCommentsPlaced)} of ${comments.length} comments placed`);
487
492
  }
493
+ if (totalCommentsLowConfidence > 0) {
494
+ summaryLines.push(`${chalk.yellow(totalCommentsLowConfidence)} placed approximately (review with verify-anchors)`);
495
+ }
488
496
  if (totalCommentsDeduped > 0) {
489
497
  summaryLines.push(`${chalk.cyan(totalCommentsDeduped)} already present (skipped)`);
490
498
  }
@@ -587,12 +595,12 @@ async function syncCommentsOnly(
587
595
  }
588
596
 
589
597
  const firstBoundaryStart = boundaries[0].start;
590
- const results: Array<{ file: string; placed: number; deduped: number; unmatched: number; skipped: boolean }> = [];
598
+ const results: Array<{ file: string; placed: number; lowConfidence: number; deduped: number; unmatched: number; skipped: boolean }> = [];
591
599
 
592
600
  for (const boundary of activeBoundaries) {
593
601
  const sectionPath = path.join(options.dir, boundary.file);
594
602
  if (!fs.existsSync(sectionPath)) {
595
- results.push({ file: boundary.file, placed: 0, deduped: 0, unmatched: 0, skipped: true });
603
+ results.push({ file: boundary.file, placed: 0, lowConfidence: 0, deduped: 0, unmatched: 0, skipped: true });
596
604
  continue;
597
605
  }
598
606
 
@@ -607,13 +615,13 @@ async function syncCommentsOnly(
607
615
  });
608
616
 
609
617
  if (sectionComments.length === 0) {
610
- results.push({ file: boundary.file, placed: 0, deduped: 0, unmatched: 0, skipped: false });
618
+ results.push({ file: boundary.file, placed: 0, lowConfidence: 0, deduped: 0, unmatched: 0, skipped: false });
611
619
  continue;
612
620
  }
613
621
 
614
622
  const original = fs.readFileSync(sectionPath, 'utf-8');
615
623
 
616
- const stats = { placed: 0, deduped: 0, unmatched: 0 };
624
+ const stats = { placed: 0, lowConfidence: 0, deduped: 0, unmatched: 0 };
617
625
  const annotated = insertCommentsIntoMarkdown(original, sectionComments, anchors, {
618
626
  quiet: !process.env.DEBUG,
619
627
  sectionBoundary: { start: boundary.start, end: boundary.end },
@@ -621,7 +629,7 @@ async function syncCommentsOnly(
621
629
  outStats: stats,
622
630
  });
623
631
 
624
- if (!options.dryRun && stats.placed > 0) {
632
+ if (!options.dryRun && (stats.placed > 0 || stats.lowConfidence > 0)) {
625
633
  fs.writeFileSync(sectionPath, annotated, 'utf-8');
626
634
  }
627
635
  results.push({ file: boundary.file, ...stats, skipped: false });
@@ -634,6 +642,7 @@ async function syncCommentsOnly(
634
642
  return [
635
643
  chalk.bold(r.file),
636
644
  chalk.green(`${r.placed}`),
645
+ r.lowConfidence > 0 ? chalk.yellow(`${r.lowConfidence}`) : chalk.dim('-'),
637
646
  r.deduped > 0 ? chalk.cyan(`${r.deduped}`) : chalk.dim('-'),
638
647
  r.unmatched > 0 ? chalk.yellow(`${r.unmatched}`) : chalk.dim('-'),
639
648
  chalk.dim('comments only'),
@@ -641,13 +650,14 @@ async function syncCommentsOnly(
641
650
  });
642
651
 
643
652
  console.log(fmt.table(
644
- ['File', 'Placed', 'Already', 'Unmatched', 'Mode'],
653
+ ['File', 'Placed', 'Approx', 'Already', 'Unmatched', 'Mode'],
645
654
  tableRows,
646
- { align: ['left', 'right', 'right', 'right', 'left'] },
655
+ { align: ['left', 'right', 'right', 'right', 'right', 'left'] },
647
656
  ));
648
657
  console.log();
649
658
 
650
659
  const totalPlaced = results.reduce((s, r) => s + r.placed, 0);
660
+ const totalLowConfidence = results.reduce((s, r) => s + r.lowConfidence, 0);
651
661
  const totalDeduped = results.reduce((s, r) => s + r.deduped, 0);
652
662
  const totalUnmatched = results.reduce((s, r) => s + r.unmatched, 0);
653
663
 
@@ -656,6 +666,9 @@ async function syncCommentsOnly(
656
666
  if (totalPlaced > 0) {
657
667
  lines.push(`${chalk.bold(totalPlaced)} placed at anchors`);
658
668
  }
669
+ if (totalLowConfidence > 0) {
670
+ lines.push(`${chalk.yellow(totalLowConfidence)} placed approximately (review with verify-anchors)`);
671
+ }
659
672
  if (totalDeduped > 0) {
660
673
  lines.push(`${chalk.cyan(totalDeduped)} already present (skipped to avoid duplication)`);
661
674
  }
@@ -664,12 +677,12 @@ async function syncCommentsOnly(
664
677
  }
665
678
  if (options.dryRun) {
666
679
  lines.push(chalk.yellow('Dry run — no files written'));
667
- } else if (totalPlaced > 0) {
680
+ } else if (totalPlaced > 0 || totalLowConfidence > 0) {
668
681
  lines.push(chalk.dim('Existing prose unchanged.'));
669
682
  }
670
683
  console.log(fmt.box(lines.join('\n'), { title: 'Summary', padding: 0 }));
671
684
 
672
- if (totalUnmatched > 0) {
685
+ if (totalUnmatched > 0 || totalLowConfidence > 0) {
673
686
  console.log();
674
687
  console.log(chalk.dim('Tip: run "rev verify-anchors" to see which comments drifted.'));
675
688
  }