feat(browse): unify export formats via one matrix; add PDF to Export menu
The Export context-menu offered only md↔docx↔html (a symmetric set), so PDF — which the server supports only as md→pdf — was missing. The markdown editor's DOCX/HTML/PDF buttons hardcoded their own list, so the two could drift. Introduce a single source of truth in download.js: EXPORT_MATRIX mirrors zddc/internal/convert.Convert() exactly (md→docx|html|pdf, docx→md|html, html→md|docx), exposed as download.exportTargets(ext) + download.convertUrl(). The Export submenu and the editor's buttons both consume it, so a .md file now offers PDF in the menu and the two surfaces can never disagree. PDF stays markdown-only (no docx→pdf / html→pdf path exists server-side). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
509839dba9
commit
3823946d4f
3 changed files with 58 additions and 15 deletions
|
|
@ -24,6 +24,32 @@
|
||||||
|
|
||||||
function events() { return window.app.modules.events; }
|
function events() { return window.app.modules.events; }
|
||||||
|
|
||||||
|
// Canonical document-conversion matrix — mirrors zddc/internal/convert
|
||||||
|
// Convert(): which target formats a given source extension can be exported
|
||||||
|
// to. PDF is markdown-only (md→pdf) because the server has no docx→pdf /
|
||||||
|
// html→pdf path. This is the SINGLE source of truth for both the Export
|
||||||
|
// context-menu (download.exportTargets) and the markdown editor's
|
||||||
|
// DOCX/HTML/PDF buttons (preview-markdown.js), so the two never drift.
|
||||||
|
var EXPORT_MATRIX = {
|
||||||
|
md: ['docx', 'html', 'pdf'],
|
||||||
|
docx: ['md', 'html'],
|
||||||
|
html: ['md', 'docx']
|
||||||
|
};
|
||||||
|
|
||||||
|
// exportTargets returns the formats a file of extension `ext` can be
|
||||||
|
// exported to (excludes the source format itself), or [] if `ext` is not a
|
||||||
|
// convertible source. Case-insensitive.
|
||||||
|
function exportTargets(ext) {
|
||||||
|
return EXPORT_MATRIX[String(ext || '').toLowerCase()] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertUrl maps a source path/URL to its sibling virtual-conversion URL
|
||||||
|
// (foo.md → foo.pdf). zddc-server recognises the sibling-extension pattern
|
||||||
|
// and converts on the fly. Shared by exportFile and the editor buttons.
|
||||||
|
function convertUrl(path, fmt) {
|
||||||
|
return String(path || '').replace(/\.[^./]+$/, '') + '.' + fmt;
|
||||||
|
}
|
||||||
|
|
||||||
function isHiddenName(name) {
|
function isHiddenName(name) {
|
||||||
return name.length === 0 || name[0] === '.' || name[0] === '_';
|
return name.length === 0 || name[0] === '.' || name[0] === '_';
|
||||||
}
|
}
|
||||||
|
|
@ -202,8 +228,8 @@
|
||||||
events().statusError('No path for ' + node.name);
|
events().statusError('No path for ' + node.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var url = path.replace(/\.[^./]+$/, '') + '.' + fmt;
|
var url = convertUrl(path, fmt);
|
||||||
var name = node.name.replace(/\.[^./]+$/, '') + '.' + fmt;
|
var name = convertUrl(node.name, fmt);
|
||||||
events().statusInfo('Exporting ' + name + '…');
|
events().statusInfo('Exporting ' + name + '…');
|
||||||
downloadUrl(name, url);
|
downloadUrl(name, url);
|
||||||
setTimeout(function () { events().statusClear(); }, 2500);
|
setTimeout(function () { events().statusClear(); }, 2500);
|
||||||
|
|
@ -212,6 +238,8 @@
|
||||||
window.app.modules.download = {
|
window.app.modules.download = {
|
||||||
downloadFile: downloadFile,
|
downloadFile: downloadFile,
|
||||||
downloadFolder: downloadFolder,
|
downloadFolder: downloadFolder,
|
||||||
exportFile: exportFile
|
exportFile: exportFile,
|
||||||
|
exportTargets: exportTargets,
|
||||||
|
convertUrl: convertUrl
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,16 @@
|
||||||
function appliesToFolderLike(node) { return !!(node && (node.isDir || node.isZip)); }
|
function appliesToFolderLike(node) { return !!(node && (node.isDir || node.isZip)); }
|
||||||
function appliesToFile(node) { return !!(node && !node.isDir && !node.isZip); }
|
function appliesToFile(node) { return !!(node && !node.isDir && !node.isZip); }
|
||||||
|
|
||||||
// Formats the Export submenu offers for a file (server-side conversion):
|
// The Export submenu's convertible-format set comes from the download
|
||||||
// a file of one of these extensions can be exported as the other two.
|
// module's canonical matrix (download.exportTargets), which mirrors the
|
||||||
var EXPORT_FORMATS = ['md', 'docx', 'html'];
|
// server's conversion matrix — the single source of truth shared with the
|
||||||
|
// markdown editor's DOCX/HTML/PDF buttons. exportTargets(ext) returns the
|
||||||
|
// target formats for a source extension (e.g. md → docx, html, pdf), or []
|
||||||
|
// when the extension isn't a convertible source.
|
||||||
|
function exportTargets(ext) {
|
||||||
|
var d = window.app.modules.download;
|
||||||
|
return (d && d.exportTargets) ? d.exportTargets(ext) : [];
|
||||||
|
}
|
||||||
function cap() { return window.zddc && window.zddc.cap; }
|
function cap() { return window.zddc && window.zddc.cap; }
|
||||||
|
|
||||||
function canVerb(node, verb) {
|
function canVerb(node, verb) {
|
||||||
|
|
@ -181,9 +188,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Export submenu: a folder offers ".zip" (both modes); a md/docx/html
|
// Export submenu: a folder offers ".zip" (both modes); a convertible
|
||||||
// file offers the OTHER two formats (server-side conversion, so
|
// file (md/docx/html) offers its server-side conversion targets —
|
||||||
// server mode only). A zip is already an archive — no Export.
|
// md → docx/html/pdf, docx → md/html, html → md/docx (server mode
|
||||||
|
// only). A zip is already an archive — no Export.
|
||||||
id: 'export', group: 'io', surfaces: ['row'],
|
id: 'export', group: 'io', surfaces: ['row'],
|
||||||
label: 'Export',
|
label: 'Export',
|
||||||
appliesTo: function (ctx) {
|
appliesTo: function (ctx) {
|
||||||
|
|
@ -191,7 +199,7 @@
|
||||||
if (!n || n.virtual) return false;
|
if (!n || n.virtual) return false;
|
||||||
if (n.isDir) return true;
|
if (n.isDir) return true;
|
||||||
if (n.isZip) return false;
|
if (n.isZip) return false;
|
||||||
return isServer() && EXPORT_FORMATS.indexOf((n.ext || '').toLowerCase()) !== -1;
|
return isServer() && exportTargets(n.ext).length > 0;
|
||||||
},
|
},
|
||||||
items: function (ctx) {
|
items: function (ctx) {
|
||||||
var n = ctx.node;
|
var n = ctx.node;
|
||||||
|
|
@ -200,8 +208,8 @@
|
||||||
if (n.isDir) {
|
if (n.isDir) {
|
||||||
return [{ label: '.zip', action: function () { d.downloadFolder(n); } }];
|
return [{ label: '.zip', action: function () { d.downloadFolder(n); } }];
|
||||||
}
|
}
|
||||||
var cur = (n.ext || '').toLowerCase();
|
// exportTargets already excludes the source format.
|
||||||
return EXPORT_FORMATS.filter(function (f) { return f !== cur; }).map(function (fmt) {
|
return exportTargets(n.ext).map(function (fmt) {
|
||||||
return { label: '.' + fmt, action: function () { d.exportFile(n, fmt); } };
|
return { label: '.' + fmt, action: function () { d.exportFile(n, fmt); } };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -465,11 +465,18 @@
|
||||||
// and routes through ServeConverted. Cleaner than the
|
// and routes through ServeConverted. Cleaner than the
|
||||||
// old `?convert=` query form — right-clicking the link
|
// old `?convert=` query form — right-clicking the link
|
||||||
// gives a sensible "Save as <file>.docx" prompt.
|
// gives a sensible "Save as <file>.docx" prompt.
|
||||||
var mdUrlBase = node.url.replace(/\.md$/i, '');
|
//
|
||||||
['docx', 'html', 'pdf'].forEach(function (fmt) {
|
// Format set + URL come from the download module's canonical
|
||||||
|
// conversion matrix (download.exportTargets / convertUrl) — the
|
||||||
|
// SAME source of truth the Export context-menu uses, so the
|
||||||
|
// editor's buttons and the menu never offer different formats.
|
||||||
|
var dl = window.app.modules.download;
|
||||||
|
var mdTargets = (dl && dl.exportTargets) ? dl.exportTargets('md') : ['docx', 'html', 'pdf'];
|
||||||
|
mdTargets.forEach(function (fmt) {
|
||||||
var a = document.createElement('a');
|
var a = document.createElement('a');
|
||||||
a.className = 'btn btn-sm btn-secondary md-shell__download';
|
a.className = 'btn btn-sm btn-secondary md-shell__download';
|
||||||
a.href = mdUrlBase + '.' + fmt;
|
a.href = (dl && dl.convertUrl) ? dl.convertUrl(node.url, fmt)
|
||||||
|
: node.url.replace(/\.md$/i, '') + '.' + fmt;
|
||||||
// target=_blank: clicks open in a new tab. The server
|
// target=_blank: clicks open in a new tab. The server
|
||||||
// sends Content-Disposition: inline, so the new tab
|
// sends Content-Disposition: inline, so the new tab
|
||||||
// either renders (HTML → web page; PDF → browser's
|
// either renders (HTML → web page; PDF → browser's
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue