Skip to content

Commit 22c7ae9

Browse files
committed
release: v2.10.6 sanitize highlight output and import/heading fixes
- Sec: filter hljs output to span+class before dangerouslySetInnerHTML so the syntax-highlight path can't bypass rehype-sanitize. - Enhance: accept .markdown/.mdown/.mkd on drag-drop and button import. - Fix: preserve snake_case underscores in the suggested PDF filename. - Fix: re-clamp editor pane width on window resize. Bundles the prior bug-hunt fixes (FileReader error handling, DragBar mouse-grab snap, async rejection handling) into this patch release.
1 parent 0ef9c05 commit 22c7ae9

8 files changed

Lines changed: 64 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
**Labels:** **Build**, **Chore**, **CI**, **Docs**, **Enhance**, **Feat**, **Fix**, **Perf**, **Revert**, **Sec**, **Style**; add **(WIP)** for incomplete work.
44

5+
## [2.10.6] - 2026-06-02
6+
7+
- **Enhance:** Accept `.markdown`, `.mdown`, and `.mkd` files on import, not just `.md`.
8+
- **Fix:** Failed or aborted file reads no longer lock out imports or fail silently.
9+
- **Fix:** Dragging the divider no longer snaps the editor pane by up to 15px.
10+
- **Fix:** Re-clamp the editor pane width on window resize so it cannot overflow.
11+
- **Fix:** Keep `snake_case` underscores in the heading used as the suggested PDF filename.
12+
- **Fix:** Print proceeds even when Mermaid rendering rejects, and async rejections are handled.
13+
- **Sec:** Sanitize syntax-highlight output before injection so it cannot bypass the markdown sanitizer.
14+
515
## [2.10.5] - 2026-06-02
616

717
- **Chore:** Add `dependabot.yml` so npm and github-actions updates arrive as weekly grouped PRs after the npm migration.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "md2pdf",
3-
"version": "2.10.5",
3+
"version": "2.10.6",
44
"packageManager": "npm@10.9.3",
55
"description": "Mobile-friendly Markdown to PDF in your browser with GFM, syntax highlighting, and Mermaid. Works offline; nothing is uploaded for conversion. Fork of realdennis/md2pdf (MIT).",
66
"keywords": [

src/App/Components/Header/Upload.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export default (props) => {
1111
const files = e.currentTarget.files;
1212
if (files.length > 0) {
1313
const file = files[0];
14-
if (!/\.(md)$/i.test(file.name)) {
15-
alert('Only .md files are allowed.');
14+
if (!/\.(md|markdown|mdown|mkd)$/i.test(file.name)) {
15+
alert('Only Markdown files are allowed.');
1616
e.target.value = '';
1717
return;
1818
}
@@ -39,7 +39,7 @@ export default (props) => {
3939
type="file"
4040
style={{ display: 'none' }}
4141
onChange={onChange}
42-
accept=".md"
42+
accept=".md,.markdown,.mdown,.mkd"
4343
/>
4444
<label
4545
htmlFor="mdFile"

src/App/Components/Markdown/Previewer/Preview.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,31 @@ const highlight = (str, lang) => {
9696
return '';
9797
};
9898

99+
// hljs emits only <span class="hljs-*"> wrappers around HTML-escaped text.
100+
// That output is injected via dangerouslySetInnerHTML, i.e. *after*
101+
// rehype-sanitize has already run, so re-filter it down to span+class to keep
102+
// "sanitize is the last line" true for the syntax-highlight path too.
103+
export const sanitizeHighlightHtml = (html) => {
104+
if (typeof document === 'undefined') return '';
105+
const template = document.createElement('template');
106+
template.innerHTML = html;
107+
const walk = (parent) => {
108+
Array.from(parent.childNodes).forEach((node) => {
109+
if (node.nodeType !== 1) return;
110+
if (node.tagName !== 'SPAN') {
111+
parent.replaceChild(document.createTextNode(node.textContent), node);
112+
return;
113+
}
114+
Array.from(node.attributes).forEach((attr) => {
115+
if (attr.name !== 'class') node.removeAttribute(attr.name);
116+
});
117+
walk(node);
118+
});
119+
};
120+
walk(template.content);
121+
return template.innerHTML;
122+
};
123+
99124
/** react-markdown's defaultUrlTransform drops `tel:` (CV contact rows); keep it. */
100125
const urlTransform = (value) => {
101126
const v = String(value || '').trim();
@@ -139,7 +164,7 @@ export default ({ source, children }) => {
139164
<code
140165
className={className}
141166
dangerouslySetInnerHTML={{
142-
__html: highlight(trimmedCode, match[1]),
167+
__html: sanitizeHighlightHtml(highlight(trimmedCode, match[1])),
143168
}}
144169
/>
145170
);

src/App/Components/Markdown/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ const Markdown = ({ className }) => {
2121
const [uploading, isOver] = useDrop(markdownRef, setText);
2222
const isMobile = useIsMobile();
2323

24+
useEffect(() => {
25+
// The editor pane is flex-shrink:0 with a pixel width, so shrinking the
26+
// window would otherwise push the divider and preview off-screen. Clamp
27+
// the stored width down (never up) to leave room for the preview.
28+
const handleResize = () => {
29+
setWidth((w) => Math.min(w, Math.max(200, window.innerWidth - 200)));
30+
};
31+
window.addEventListener('resize', handleResize);
32+
return () => window.removeEventListener('resize', handleResize);
33+
}, []);
34+
2435
useEffect(() => {
2536
const handleMouseUp = () => setDrag(false);
2637
const handleTouchEnd = () => setDrag(false);

src/App/Container/Hooks/useDrop.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function useDrop(ref, onLoad = () => {}) {
3838
files &&
3939
files[0] &&
4040
files[0].name &&
41-
/\.(md)$/i.test(files[0].name) &&
41+
/\.(md|markdown|mdown|mkd)$/i.test(files[0].name) &&
4242
files[0].size <= MAX_FILE_SIZE &&
4343
!uploadingRef.current
4444
) {

src/App/Lib/printTitle.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ const stripInlineMarkdown = (text) =>
1111
.replace(/`([^`]+)`/g, '$1')
1212
.replace(/\*\*([^*]+)\*\*/g, '$1')
1313
.replace(/\*([^*]+)\*/g, '$1')
14-
.replace(/__([^_]+)__/g, '$1')
15-
.replace(/_([^_]+)_/g, '$1')
14+
// Underscores only mark emphasis when not intraword (CommonMark), so the
15+
// boundary guards keep snake_case identifiers in headings intact.
16+
.replace(/(^|[^\w])__([^_]+)__(?!\w)/g, '$1$2')
17+
.replace(/(^|[^\w])_([^_]+)_(?!\w)/g, '$1$2')
1618
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
1719
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
1820
.replace(/<\/?[^>]+>/g, '');

src/App/Lib/printTitle.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ describe('extractHeading', () => {
7272
expect(extractHeading('# foo/bar:baz?')).toBe('foo bar baz');
7373
});
7474

75+
test('keeps intraword underscores in snake_case headings', () => {
76+
expect(extractHeading('# my_snake_case_var')).toBe('my_snake_case_var');
77+
});
78+
79+
test('still strips underscore emphasis at word boundaries', () => {
80+
expect(extractHeading('# _italic_ word')).toBe('italic word');
81+
});
82+
7583
test('returns empty string when no heading found', () => {
7684
expect(extractHeading('just prose, nothing else')).toBe('');
7785
});

0 commit comments

Comments
 (0)