mirror of
https://github.com/jgm/pandoc.git
synced 2026-08-29 01:41:20 +08:00
1513 lines
57 KiB
JavaScript
1513 lines
57 KiB
JavaScript
import { zipSync, unzipSync, strToU8, strFromU8 } from 'https://esm.sh/fflate@0.8.2';
|
|
|
|
// Make fflate available globally
|
|
window.fflate = { zipSync, unzipSync, strToU8, strFromU8 };
|
|
|
|
// Pandoc loading - starts immediately in background, but doesn't block UI
|
|
let pandocReadyPromise = null;
|
|
let onFormatsLoaded = null; // Callback to update Vue app when formats load
|
|
|
|
function loadPandoc() {
|
|
// Only load once
|
|
if (pandocReadyPromise) return pandocReadyPromise;
|
|
|
|
pandocReadyPromise = (async () => {
|
|
// Dynamic import so WASM loading happens in background
|
|
const { convert, query } = await import("./pandoc.js?sha1=SHA1_PANDOC_JS");
|
|
window.pandocModule = { convert, query };
|
|
|
|
// Query version and formats
|
|
const pandocVersion = await query({ query: "version" });
|
|
document.getElementById("pandoc-version").innerText = pandocVersion;
|
|
|
|
const inputFormats = await query({ query: "input-formats" });
|
|
const outputFormats = await query({ query: "output-formats" });
|
|
|
|
// Update Vue app if callback is registered
|
|
if (onFormatsLoaded) {
|
|
onFormatsLoaded(inputFormats, outputFormats);
|
|
}
|
|
})();
|
|
|
|
return pandocReadyPromise;
|
|
}
|
|
|
|
// Start loading immediately in background (fire and forget)
|
|
loadPandoc().catch(err => console.error("Failed to load pandoc:", err));
|
|
|
|
// Lazy-load typst library only when needed
|
|
let typstLoaded = false;
|
|
let typstLoadingPromise = null;
|
|
|
|
window.loadTypst = async function() {
|
|
if (typstLoaded && typeof $typst !== 'undefined') return;
|
|
if (typstLoadingPromise) return typstLoadingPromise;
|
|
|
|
typstLoadingPromise = new Promise((resolve, reject) => {
|
|
const typstScript = document.createElement('script');
|
|
typstScript.type = 'module';
|
|
typstScript.src = 'https://cdn.jsdelivr.net/npm/@myriaddreamin/typst-all-in-one.ts@0.7.0-rc2/dist/esm/index.js';
|
|
typstScript.onload = () => {
|
|
const checkTypst = () => {
|
|
if (typeof $typst !== 'undefined') {
|
|
typstLoaded = true;
|
|
resolve();
|
|
} else {
|
|
setTimeout(checkTypst, 100);
|
|
}
|
|
};
|
|
checkTypst();
|
|
};
|
|
typstScript.onerror = () => reject(new Error('Failed to load Typst library'));
|
|
document.head.appendChild(typstScript);
|
|
});
|
|
return typstLoadingPromise;
|
|
};
|
|
|
|
// Petite Vue app definition
|
|
window.pandocApp = function() {
|
|
return {
|
|
// State
|
|
pandocReady: false,
|
|
waitingForPandoc: false,
|
|
selectedExample: '',
|
|
showExamplesBar: false,
|
|
inputMode: 'file',
|
|
files: {},
|
|
fileOrder: [],
|
|
auxFiles: {},
|
|
textInput: '',
|
|
inputFormat: 'auto',
|
|
outputFormat: 'auto',
|
|
outputFilename: '',
|
|
activeTab: 'general',
|
|
dragOver: false,
|
|
fileDraggingIdx: -1,
|
|
fileDragOverIdx: -1,
|
|
|
|
// Options
|
|
opts: {
|
|
standalone: true,
|
|
toc: false,
|
|
numberSections: false,
|
|
fileScope: false,
|
|
extractMedia: false,
|
|
tocDepth: '3',
|
|
shiftHeading: '0',
|
|
tabStop: '4',
|
|
preserveTabs: false,
|
|
eol: '',
|
|
dpi: '',
|
|
numberOffset: '',
|
|
resourcePath: '',
|
|
defaultImageExtension: '',
|
|
stripComments: false,
|
|
trackChanges: '',
|
|
citationMethod: '',
|
|
mathMethod: '',
|
|
highlightStyle: '',
|
|
indentedCodeClasses: '',
|
|
selfContained: false,
|
|
htmlQTags: false,
|
|
sectionDivs: false,
|
|
asciiHtml: false,
|
|
titlePrefix: '',
|
|
idPrefix: '',
|
|
emailObfuscation: '',
|
|
referenceLinks: false,
|
|
referenceLocation: '',
|
|
markdownHeadings: '',
|
|
referenceLinksRst: false,
|
|
listTables: false,
|
|
lof: false,
|
|
lot: false,
|
|
figureCaptionPosition: '',
|
|
tableCaptionPosition: '',
|
|
topLevelDivision: '',
|
|
ascii: false,
|
|
epubTitlePage: true,
|
|
epubSubdirectory: '',
|
|
splitLevel: '',
|
|
chunkTemplate: '',
|
|
ipynbOutput: '',
|
|
slideLevel: '',
|
|
incremental: false,
|
|
wrap: 'auto',
|
|
columns: '',
|
|
linkImages: false
|
|
},
|
|
|
|
// Metadata
|
|
customMetadata: [{ key: '', value: '' }],
|
|
customVariables: [{ key: '', value: '' }],
|
|
|
|
// Resource files
|
|
bibFile: null,
|
|
cslFile: null,
|
|
abbrevFile: null,
|
|
abbreviationsFile: null,
|
|
cssFiles: [],
|
|
highlightThemeFile: null,
|
|
syntaxDefinitions: [],
|
|
templateFile: null,
|
|
referenceDoc: null,
|
|
metadataFile: null,
|
|
epubCoverImage: null,
|
|
epubMetadataFile: null,
|
|
epubFonts: [],
|
|
headerFiles: [],
|
|
beforeBodyFiles: [],
|
|
afterBodyFiles: [],
|
|
luaFilters: [],
|
|
filterDraggingIdx: -1,
|
|
filterDragOverIdx: -1,
|
|
|
|
// CSL
|
|
cslStyleName: '',
|
|
cslStylesList: [],
|
|
cslStylesLoaded: false,
|
|
|
|
// Extensions
|
|
inputExtensions: {},
|
|
outputExtensions: {},
|
|
inputExtensionsList: [],
|
|
outputExtensionsList: [],
|
|
|
|
// Output
|
|
hasChanges: false,
|
|
isConverting: false,
|
|
showOutput: false,
|
|
output: null,
|
|
outputFilenameActual: '',
|
|
outputPreview: '',
|
|
messages: [],
|
|
verbosity: 'info',
|
|
copyBtnText: '📋 Copy to Clipboard',
|
|
mediaZip: null,
|
|
|
|
// Format names: maps format ID to human-readable name
|
|
// Some formats have different names for input vs output
|
|
formatNames: {
|
|
ansi: 'ANSI terminal',
|
|
asciidoc: 'modern AsciiDoc',
|
|
asciidoc_legacy: 'AsciiDoc for asciidoc-py',
|
|
asciidoctor: 'Asciidoctor (= modern AsciiDoc)',
|
|
bbcode: 'BBCode',
|
|
beamer: 'LaTeX Beamer slides',
|
|
biblatex: 'BibLaTeX bibliography',
|
|
bibtex: 'BibTeX bibliography',
|
|
bits: 'BITS XML, alias for jats',
|
|
chunkedhtml: 'zip of linked HTML files',
|
|
commonmark: 'CommonMark Markdown',
|
|
commonmark_x: 'CommonMark with extensions',
|
|
context: 'ConTeXt',
|
|
creole: 'Creole 1.0',
|
|
csljson: 'CSL JSON bibliography',
|
|
csv: 'CSV table',
|
|
djot: 'Djot markup',
|
|
docbook: 'DocBook v4',
|
|
docbook5: 'DocBook v5',
|
|
docx: 'Word',
|
|
dokuwiki: 'DokuWiki markup',
|
|
dzslides: 'DZSlides HTML slides',
|
|
endnotexml: 'EndNote XML bibliography',
|
|
epub: 'EPUB v3',
|
|
epub2: 'EPUB v2',
|
|
epub3: 'EPUB v3',
|
|
fb2: 'FictionBook2',
|
|
gfm: 'GitHub-Flavored Markdown',
|
|
haddock: 'Haddock markup',
|
|
html: 'HTML',
|
|
html4: 'XHTML 1.0 Transitional',
|
|
html5: 'HTML',
|
|
icml: 'InDesign ICML',
|
|
ipynb: 'Jupyter notebook',
|
|
jats: 'JATS XML',
|
|
jira: 'Jira/Confluence wiki markup',
|
|
json: 'JSON version of native AST',
|
|
latex: 'LaTeX',
|
|
man: 'roff man',
|
|
markdown: "Pandoc's Markdown",
|
|
markdown_mmd: 'MultiMarkdown',
|
|
markdown_phpextra: 'PHP Markdown Extra',
|
|
markdown_strict: 'original unextended Markdown',
|
|
markua: 'Markua',
|
|
mdoc: 'mdoc manual page markup',
|
|
mediawiki: 'MediaWiki markup',
|
|
ms: 'roff ms',
|
|
muse: 'Muse',
|
|
native: 'native Haskell',
|
|
odt: 'OpenDocument text',
|
|
opendocument: 'OpenDocument XML',
|
|
opml: 'OPML',
|
|
org: 'Emacs Org mode',
|
|
pdf: 'PDF via Typst',
|
|
plain: 'plain text',
|
|
pod: 'Perl POD',
|
|
pptx: 'PowerPoint',
|
|
revealjs: 'reveal.js HTML slides',
|
|
ris: 'RIS bibliography',
|
|
rst: 'reStructuredText',
|
|
rtf: 'Rich Text Format',
|
|
s5: 'S5 HTML slides',
|
|
slideous: 'Slideous HTML slides',
|
|
slidy: 'Slidy HTML slides',
|
|
t2t: 'txt2tags',
|
|
tei: 'TEI Simple',
|
|
texinfo: 'GNU Texinfo',
|
|
textile: 'Textile',
|
|
tikiwiki: 'TikiWiki markup',
|
|
tsv: 'TSV table',
|
|
twiki: 'TWiki markup',
|
|
typst: 'Typst',
|
|
vimdoc: 'Vimdoc',
|
|
vimwiki: 'Vimwiki',
|
|
xlsx: 'Excel spreadsheet',
|
|
xml: 'XML version of native AST',
|
|
xwiki: 'XWiki markup',
|
|
zimwiki: 'ZimWiki markup'
|
|
},
|
|
|
|
// List of supported formats (populated when pandoc loads)
|
|
inputFormats: [],
|
|
outputFormats: [],
|
|
formatsLoaded: false,
|
|
|
|
// Format constants
|
|
formatByExtension: {
|
|
'md': 'markdown', 'markdown': 'markdown', 'mkd': 'markdown',
|
|
'html': 'html', 'htm': 'html',
|
|
'tex': 'latex', 'latex': 'latex',
|
|
'rst': 'rst', 'org': 'org', 'docx': 'docx', 'odt': 'odt',
|
|
'epub': 'epub', 'txt': 'markdown', 'json': 'json', 'ipynb': 'ipynb',
|
|
'xml': 'docbook', 'wiki': 'mediawiki', 'textile': 'textile',
|
|
'rtf': 'rtf', 'bib': 'bibtex', 'csv': 'csv', 'tsv': 'tsv',
|
|
'typ': 'typst', 'typst': 'typst', 'pptx': 'pptx'
|
|
},
|
|
|
|
extensionByFormat: {
|
|
'html': 'html', 'html5': 'html', 'html4': 'html', 'chunkedhtml': 'zip',
|
|
'markdown': 'md', 'markdown_strict': 'md', 'markdown_mmd': 'md', 'markdown_phpextra': 'md',
|
|
'gfm': 'md', 'commonmark': 'md', 'commonmark_x': 'md',
|
|
'latex': 'tex', 'beamer': 'tex', 'context': 'tex',
|
|
'pdf': 'pdf', 'docx': 'docx', 'odt': 'odt',
|
|
'epub': 'epub', 'epub2': 'epub', 'epub3': 'epub',
|
|
'rst': 'rst', 'org': 'org', 'plain': 'txt',
|
|
'json': 'json', 'native': 'native',
|
|
'docbook': 'xml', 'docbook4': 'xml', 'docbook5': 'xml',
|
|
'jats': 'xml', 'tei': 'xml', 'man': '1', 'rtf': 'rtf',
|
|
'textile': 'textile', 'mediawiki': 'wiki',
|
|
'asciidoc': 'adoc', 'asciidoctor': 'adoc', 'asciidoc_legacy': 'adoc',
|
|
'revealjs': 'html', 'slidy': 'html', 'slideous': 'html', 'dzslides': 'html', 's5': 'html',
|
|
'ipynb': 'ipynb', 'typst': 'typ', 'texinfo': 'texi', 'ms': 'ms', 'icml': 'icml',
|
|
'opml': 'opml', 'bibtex': 'bib', 'biblatex': 'bib', 'csljson': 'json',
|
|
'pptx': 'pptx', 'djot': 'dj', 'fb2': 'fb2', 'opendocument': 'xml', 'vimdoc': 'txt'
|
|
},
|
|
|
|
slideFormats: ['revealjs', 'slidy', 'slideous', 'dzslides', 's5', 'beamer', 'pptx'],
|
|
htmlFormats: ['html', 'html4', 'html5', 'revealjs', 'slidy', 'slideous', 'dzslides', 's5', 'epub', 'epub2', 'epub3', 'chunkedhtml'],
|
|
docFormats: ['docx', 'odt', 'pptx'],
|
|
binaryFormats: ['docx', 'odt', 'pptx', 'epub', 'epub2', 'epub3', 'pdf', 'chunkedhtml'],
|
|
markdownFormats: ['markdown', 'markdown_strict', 'markdown_mmd', 'markdown_phpextra', 'gfm', 'commonmark', 'commonmark_x'],
|
|
captionPositionFormats: ['html', 'html4', 'html5', 'latex', 'beamer', 'docx', 'odt', 'typst', 'pdf'],
|
|
asciiFormats: ['html', 'html4', 'html5', 'markdown', 'markdown_strict', 'markdown_mmd', 'markdown_phpextra', 'gfm', 'commonmark', 'commonmark_x', 'docbook', 'docbook4', 'docbook5', 'jats', 'man', 'ms', 'latex', 'beamer'],
|
|
topLevelDivisionFormats: ['latex', 'beamer', 'context', 'docbook', 'docbook4', 'docbook5', 'tei'],
|
|
listOfFormats: ['latex', 'beamer', 'context'],
|
|
latexCitationFormats: ['latex', 'beamer', 'context'],
|
|
|
|
// Template variables by category
|
|
templateVariablesByCategory: {
|
|
all: ['title', 'author', 'date', 'subtitle', 'abstract', 'abstract-title', 'keywords', 'subject', 'description', 'category', 'lang', 'dir', 'header-includes', 'include-before', 'include-after', 'toc-title'],
|
|
html: ['document-css', 'mainfont', 'fontsize', 'fontcolor', 'linkcolor', 'monofont', 'monobackgroundcolor', 'linestretch', 'maxwidth', 'backgroundcolor', 'margin-left', 'margin-right', 'margin-top', 'margin-bottom'],
|
|
htmlSlides: ['institute', 'revealjs-url', 's5-url', 'slidy-url', 'slideous-url', 'title-slide-attributes'],
|
|
beamer: ['aspectratio', 'beameroption', 'institute', 'logo', 'navigation', 'section-titles', 'theme', 'colortheme', 'fonttheme', 'innertheme', 'outertheme', 'themeoptions', 'titlegraphic'],
|
|
latex: ['block-headings', 'classoption', 'documentclass', 'geometry', 'hyperrefoptions', 'indent', 'linestretch', 'margin-left', 'margin-right', 'margin-top', 'margin-bottom', 'pagestyle', 'papersize', 'secnumdepth', 'fontenc', 'fontfamily', 'fontfamilyoptions', 'fontsize', 'mainfont', 'sansfont', 'monofont', 'mathfont', 'CJKmainfont', 'mainfontoptions', 'sansfontoptions', 'monofontoptions', 'colorlinks', 'linkcolor', 'filecolor', 'citecolor', 'urlcolor', 'toccolor', 'links-as-notes', 'lof', 'lot', 'thanks', 'toc-depth', 'biblatexoptions', 'biblio-style', 'biblio-title', 'natbiboptions'],
|
|
context: ['fontsize', 'headertext', 'footertext', 'indenting', 'interlinespace', 'layout', 'linkcolor', 'contrastcolor', 'linkstyle', 'lof', 'lot', 'mainfont', 'sansfont', 'monofont', 'mathfont', 'margin-left', 'margin-right', 'margin-top', 'margin-bottom', 'pagenumbering', 'papersize', 'whitespace'],
|
|
typst: ['papersize', 'mainfont', 'fontsize', 'section-numbering', 'page-numbering', 'columns', 'thanks', 'mathfont', 'codefont', 'linestretch', 'linkcolor', 'filecolor', 'citecolor', 'margin'],
|
|
ms: ['fontfamily', 'indent', 'lineheight', 'pointsize'],
|
|
man: ['adjusting', 'footer', 'header', 'section'],
|
|
epub: ['cover-image', 'epub-title-page'],
|
|
docx: ['reference-doc'],
|
|
pptx: ['monofont'],
|
|
odt: ['reference-doc']
|
|
},
|
|
|
|
// Format to variable categories mapping
|
|
formatVariableCategories: {
|
|
html: ['all', 'html'],
|
|
html4: ['all', 'html'],
|
|
html5: ['all', 'html'],
|
|
revealjs: ['all', 'html', 'htmlSlides'],
|
|
slidy: ['all', 'html', 'htmlSlides'],
|
|
slideous: ['all', 'html', 'htmlSlides'],
|
|
dzslides: ['all', 'html', 'htmlSlides'],
|
|
s5: ['all', 'html', 'htmlSlides'],
|
|
epub: ['all', 'html', 'epub'],
|
|
epub2: ['all', 'html', 'epub'],
|
|
epub3: ['all', 'html', 'epub'],
|
|
chunkedhtml: ['all', 'html'],
|
|
latex: ['all', 'latex'],
|
|
beamer: ['all', 'latex', 'beamer'],
|
|
context: ['all', 'context'],
|
|
typst: ['all', 'typst'],
|
|
pdf: ['all', 'typst'],
|
|
ms: ['all', 'ms'],
|
|
man: ['all', 'man'],
|
|
docx: ['all', 'docx'],
|
|
pptx: ['all', 'pptx'],
|
|
odt: ['all', 'odt'],
|
|
opendocument: ['all', 'odt']
|
|
},
|
|
|
|
// Computed-like getters
|
|
get canConvert() {
|
|
const hasInput = this.inputMode === 'file' ? this.fileOrder.length > 0 : this.textInput.trim().length > 0;
|
|
return hasInput && this.hasChanges;
|
|
},
|
|
|
|
get effectiveOutputFormat() {
|
|
if (this.outputFormat !== 'auto') return this.outputFormat;
|
|
const outFile = this.outputFilename.trim() || this.outputFilenamePlaceholder;
|
|
if (outFile && outFile !== '(preview)') {
|
|
const ext = outFile.split('.').pop().toLowerCase();
|
|
const formatByOutputExtension = {
|
|
'html': 'html', 'htm': 'html', 'md': 'markdown', 'markdown': 'markdown',
|
|
'tex': 'latex', 'pdf': 'pdf', 'docx': 'docx', 'odt': 'odt',
|
|
'epub': 'epub', 'rst': 'rst', 'org': 'org', 'txt': 'plain',
|
|
'json': 'json', 'native': 'native', 'xml': 'docbook', 'rtf': 'rtf',
|
|
'adoc': 'asciidoc', 'ipynb': 'ipynb', 'typ': 'typst', 'pptx': 'pptx',
|
|
'dj': 'djot', 'man': 'man', '1': 'man', 'ms': 'ms', 'texi': 'texinfo',
|
|
'icml': 'icml', 'opml': 'opml', 'bib': 'bibtex', 'wiki': 'mediawiki'
|
|
};
|
|
return formatByOutputExtension[ext] || 'html';
|
|
}
|
|
return 'html';
|
|
},
|
|
|
|
get outputFilenamePlaceholder() {
|
|
if (this.outputFormat !== 'auto') {
|
|
const outExt = this.extensionByFormat[this.outputFormat] || this.outputFormat;
|
|
if (this.fileOrder.length > 0) {
|
|
const baseName = this.fileOrder[0].replace(/\.[^.]+$/, '');
|
|
return `${baseName}.${outExt}`;
|
|
}
|
|
return `output.${outExt}`;
|
|
}
|
|
return '(preview)';
|
|
},
|
|
|
|
get isHtmlFormat() { return this.htmlFormats.includes(this.effectiveOutputFormat); },
|
|
get isMarkdownFormat() { return this.markdownFormats.includes(this.effectiveOutputFormat); },
|
|
get isLatexLikeFormat() { return this.latexCitationFormats.includes(this.effectiveOutputFormat); },
|
|
get supportsCaptionPosition() { return this.captionPositionFormats.includes(this.effectiveOutputFormat); },
|
|
get supportsAscii() { return this.asciiFormats.includes(this.effectiveOutputFormat); },
|
|
get supportsTopLevelDivision() { return this.topLevelDivisionFormats.includes(this.effectiveOutputFormat); },
|
|
get supportsListOf() { return this.listOfFormats.includes(this.effectiveOutputFormat); },
|
|
get isBinaryOutput() { return this.binaryFormats.includes(this.effectiveOutputFormat); },
|
|
get filteredMessages() {
|
|
if (this.verbosity === 'error') {
|
|
return this.messages.filter(m => m.type === 'error');
|
|
} else if (this.verbosity === 'warning') {
|
|
return this.messages.filter(m => m.type === 'error' || m.type === 'warning');
|
|
}
|
|
return this.messages;
|
|
},
|
|
|
|
get suggestedVariables() {
|
|
const fmt = this.effectiveOutputFormat;
|
|
const categories = this.formatVariableCategories[fmt] || ['all'];
|
|
const vars = new Set();
|
|
for (const cat of categories) {
|
|
const catVars = this.templateVariablesByCategory[cat] || [];
|
|
for (const v of catVars) vars.add(v);
|
|
}
|
|
return Array.from(vars).sort();
|
|
},
|
|
|
|
get effectiveInputFormat() {
|
|
let inFmt = this.inputFormat;
|
|
if (inFmt === 'auto' && this.fileOrder.length > 0) {
|
|
const ext = this.fileOrder[0].split('.').pop().toLowerCase();
|
|
inFmt = this.formatByExtension[ext] || '';
|
|
}
|
|
return inFmt;
|
|
},
|
|
|
|
get showTrackChangesTab() {
|
|
return this.effectiveInputFormat === 'docx';
|
|
},
|
|
|
|
get showStripComments() {
|
|
const fmt = this.effectiveInputFormat;
|
|
return fmt === 'auto' || this.markdownFormats.includes(fmt) || fmt === 'textile';
|
|
},
|
|
|
|
get showDefaultImageExtension() {
|
|
const fmt = this.effectiveInputFormat;
|
|
return fmt === 'auto' || this.markdownFormats.includes(fmt) || fmt === 'latex';
|
|
},
|
|
|
|
get showAbbreviations() {
|
|
const fmt = this.effectiveInputFormat;
|
|
return fmt === 'auto' || fmt === 'markdown';
|
|
},
|
|
|
|
get showExtensionsTab() {
|
|
return (this.inputFormat !== 'auto' && this.inputExtensionsList.length > 0) ||
|
|
(this.outputFormat !== 'auto' && this.outputExtensionsList.length > 0);
|
|
},
|
|
|
|
get showFormatTab() {
|
|
const fmt = this.effectiveOutputFormat;
|
|
return this.showStripComments || this.showDefaultImageExtension || this.showAbbreviations ||
|
|
this.isHtmlFormat || this.isMarkdownFormat || fmt === 'rst' ||
|
|
['latex', 'beamer'].includes(fmt) || ['typst', 'pdf'].includes(fmt) ||
|
|
this.supportsCaptionPosition || this.supportsAscii || this.supportsTopLevelDivision ||
|
|
this.supportsListOf || this.docFormats.includes(fmt);
|
|
},
|
|
|
|
get formatTabName() {
|
|
return 'Format-specific';
|
|
},
|
|
|
|
get isEpubFormat() { return ['epub', 'epub2', 'epub3'].includes(this.effectiveOutputFormat); },
|
|
get showChunkedTab() { return this.effectiveOutputFormat === 'chunkedhtml'; },
|
|
get showIpynbTab() { return this.effectiveOutputFormat === 'ipynb'; },
|
|
get showSlidesTab() { return this.slideFormats.includes(this.effectiveOutputFormat); },
|
|
get showReferenceDoc() { return this.docFormats.includes(this.effectiveOutputFormat); },
|
|
|
|
// Methods
|
|
getFormatLabel(fmt, direction) {
|
|
const name = this.formatNames[fmt] || fmt;
|
|
return `${fmt} (${name})`;
|
|
},
|
|
|
|
init() {
|
|
// Register callback to populate formats when pandoc loads
|
|
onFormatsLoaded = (inputFmts, outputFmts) => {
|
|
this.inputFormats = inputFmts;
|
|
this.outputFormats = outputFmts;
|
|
this.formatsLoaded = true;
|
|
};
|
|
console.log('Pandoc converter initialized (pandoc.wasm loading in background)');
|
|
},
|
|
|
|
async ensurePandocLoaded() {
|
|
if (this.pandocReady) return;
|
|
this.waitingForPandoc = true;
|
|
try {
|
|
await loadPandoc();
|
|
this.pandocReady = true;
|
|
} finally {
|
|
this.waitingForPandoc = false;
|
|
}
|
|
},
|
|
|
|
async loadExample() {
|
|
const zipPath = this.selectedExample;
|
|
if (!zipPath) return;
|
|
|
|
try {
|
|
// Fetch the example first
|
|
const response = await fetch(zipPath + "?sha1=SHA1_EXAMPLES");
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
|
|
// Ensure pandoc is loaded before processing (which auto-converts)
|
|
await this.ensurePandocLoaded();
|
|
|
|
await this.loadExampleFromBuffer(arrayBuffer, zipPath.split('/').pop());
|
|
} catch (err) {
|
|
this.messages.push({ type: 'error', text: `Failed to load example: ${err.message}` });
|
|
}
|
|
},
|
|
|
|
async uploadExample(e) {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
try {
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
e.target.value = '';
|
|
|
|
// Ensure pandoc is loaded before processing (which auto-converts)
|
|
await this.ensurePandocLoaded();
|
|
|
|
await this.loadExampleFromBuffer(arrayBuffer, file.name);
|
|
} catch (err) {
|
|
this.showOutput = true;
|
|
this.messages.push({ type: 'error', text: `Failed to load example: ${err.message}` });
|
|
}
|
|
},
|
|
|
|
async loadExampleFromBuffer(arrayBuffer, filename) {
|
|
const zipData = fflate.unzipSync(new Uint8Array(arrayBuffer));
|
|
|
|
// Clear existing state
|
|
this.files = {};
|
|
this.fileOrder = [];
|
|
this.auxFiles = {};
|
|
this.textInput = '';
|
|
this.templateFile = null;
|
|
this.referenceDoc = null;
|
|
this.bibFile = null;
|
|
this.cslFile = null;
|
|
this.cslStyleName = '';
|
|
this.cssFiles = [];
|
|
this.headerFiles = [];
|
|
this.beforeBodyFiles = [];
|
|
this.afterBodyFiles = [];
|
|
this.highlightThemeFile = null;
|
|
this.syntaxDefinitions = [];
|
|
this.abbrevFile = null;
|
|
this.abbreviationsFile = null;
|
|
this.metadataFile = null;
|
|
this.epubCoverImage = null;
|
|
this.epubMetadataFile = null;
|
|
this.epubFonts = [];
|
|
this.luaFilters = [];
|
|
|
|
let optionsJson = null;
|
|
let stdinContent = null;
|
|
const extractedFiles = {};
|
|
|
|
// First pass: extract all files
|
|
for (const [name, data] of Object.entries(zipData)) {
|
|
if (name.endsWith('/') || data.length === 0) continue;
|
|
const fname = name.split('/').pop();
|
|
|
|
if (fname === 'options.json') {
|
|
const txt = fflate.strFromU8(data);
|
|
optionsJson = JSON.parse(txt);
|
|
} else if (fname === 'stdin') {
|
|
stdinContent = fflate.strFromU8(data);
|
|
} else {
|
|
extractedFiles[fname] = new File([data], fname);
|
|
}
|
|
}
|
|
|
|
// Helper to get file and remove from extractedFiles
|
|
const takeFile = (name) => {
|
|
if (name && extractedFiles[name]) {
|
|
const f = extractedFiles[name];
|
|
delete extractedFiles[name];
|
|
return f;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// Helper for arrays of filenames
|
|
const takeFiles = (names) => {
|
|
if (!names) return [];
|
|
const arr = Array.isArray(names) ? names : [names];
|
|
return arr.map(takeFile).filter(f => f);
|
|
};
|
|
|
|
// Route files based on options.json
|
|
if (optionsJson) {
|
|
// Template
|
|
this.templateFile = takeFile(optionsJson.template);
|
|
|
|
// Reference doc
|
|
this.referenceDoc = takeFile(optionsJson['reference-doc']);
|
|
|
|
// Bibliography
|
|
const bibFiles = takeFiles(optionsJson.bibliography);
|
|
if (bibFiles.length > 0) this.bibFile = bibFiles[0];
|
|
|
|
// CSL style
|
|
this.cslFile = takeFile(optionsJson.csl);
|
|
|
|
// CSS files
|
|
this.cssFiles = takeFiles(optionsJson.css);
|
|
|
|
// Include files
|
|
this.headerFiles = takeFiles(optionsJson['include-in-header']);
|
|
this.beforeBodyFiles = takeFiles(optionsJson['include-before-body']);
|
|
this.afterBodyFiles = takeFiles(optionsJson['include-after-body']);
|
|
|
|
// Highlight theme (if it's a file)
|
|
const hlStyle = optionsJson['highlight-style'];
|
|
if (hlStyle && extractedFiles[hlStyle]) {
|
|
this.highlightThemeFile = takeFile(hlStyle);
|
|
this.opts.highlightStyle = 'custom';
|
|
} else if (hlStyle) {
|
|
this.opts.highlightStyle = hlStyle;
|
|
}
|
|
|
|
// Syntax definitions
|
|
this.syntaxDefinitions = takeFiles(optionsJson['syntax-definition']);
|
|
|
|
// Abbreviations
|
|
this.abbrevFile = takeFile(optionsJson.abbreviations);
|
|
|
|
// Metadata file
|
|
this.metadataFile = takeFile(optionsJson['metadata-file']);
|
|
|
|
// EPUB cover image
|
|
this.epubCoverImage = takeFile(optionsJson['epub-cover-image']);
|
|
|
|
// EPUB fonts
|
|
this.epubFonts = takeFiles(optionsJson['epub-fonts']);
|
|
|
|
// Lua filters
|
|
this.luaFilters = takeFiles(optionsJson.filters);
|
|
|
|
// Input files go to main files area
|
|
const inputFileNames = optionsJson['input-files'];
|
|
if (inputFileNames) {
|
|
const inputArr = Array.isArray(inputFileNames) ? inputFileNames : [inputFileNames];
|
|
for (const name of inputArr) {
|
|
const f = takeFile(name);
|
|
if (f) {
|
|
this.files[name] = f;
|
|
this.fileOrder.push(name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set input format
|
|
if (optionsJson.from) {
|
|
this.inputFormat = optionsJson.from.split(/[+-]/)[0];
|
|
}
|
|
// Set output format
|
|
if (optionsJson.to) {
|
|
this.outputFormat = optionsJson.to.split(/[+-]/)[0];
|
|
}
|
|
// Set standalone
|
|
if (optionsJson.standalone !== undefined) {
|
|
this.opts.standalone = optionsJson.standalone;
|
|
}
|
|
// Other options
|
|
if (optionsJson['wrap']) this.opts.wrap = optionsJson['wrap'];
|
|
if (optionsJson['table-of-contents']) this.opts.toc = true;
|
|
if (optionsJson['number-sections']) this.opts.numberSections = true;
|
|
if (optionsJson.citeproc) this.opts.citationMethod = 'citeproc';
|
|
if (optionsJson['embed-resources']) this.opts.selfContained = true;
|
|
if (optionsJson['mathml']) this.opts.mathMethod = 'mathml';
|
|
}
|
|
|
|
// Remaining files go to auxiliary files
|
|
for (const [name, file] of Object.entries(extractedFiles)) {
|
|
this.auxFiles[name] = file;
|
|
}
|
|
|
|
// Handle stdin content
|
|
if (stdinContent) {
|
|
this.inputMode = 'text';
|
|
this.textInput = stdinContent;
|
|
} else if (this.fileOrder.length > 0) {
|
|
this.inputMode = 'file';
|
|
}
|
|
|
|
// Auto-convert after loading example
|
|
await this.convert();
|
|
|
|
// Update extensions UI after conversion completes
|
|
this.onInputFormatChange();
|
|
this.onOutputFormatChange();
|
|
},
|
|
|
|
async downloadAsExample() {
|
|
const zipFiles = {};
|
|
|
|
// Build options for the example
|
|
const opts = {};
|
|
if (this.inputFormat !== 'auto') opts.from = this.inputFormat;
|
|
if (this.outputFormat !== 'auto') opts.to = this.outputFormat;
|
|
if (this.opts.standalone) opts.standalone = true;
|
|
if (this.opts.toc) opts['table-of-contents'] = true;
|
|
if (this.opts.numberSections) opts['number-sections'] = true;
|
|
if (this.opts.fileScope) opts['file-scope'] = true;
|
|
if (this.opts.citationMethod === 'citeproc') opts.citeproc = true;
|
|
if (this.opts.mathMethod === 'mathml') opts.mathml = true;
|
|
if (this.opts.selfContained) opts['embed-resources'] = true;
|
|
if (this.opts.highlightStyle && this.opts.highlightStyle !== 'custom') {
|
|
opts['highlight-style'] = this.opts.highlightStyle;
|
|
}
|
|
|
|
// Helper to add a file to the zip
|
|
const addFile = async (file, name) => {
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
zipFiles[name] = new Uint8Array(arrayBuffer);
|
|
};
|
|
|
|
// Input files or stdin
|
|
if (this.inputMode === 'text' && this.textInput.trim()) {
|
|
zipFiles['stdin'] = fflate.strToU8(this.textInput);
|
|
} else if (this.inputMode === 'file' && this.fileOrder.length > 0) {
|
|
opts['input-files'] = this.fileOrder;
|
|
for (const name of this.fileOrder) {
|
|
if (this.files[name]) await addFile(this.files[name], name);
|
|
}
|
|
}
|
|
|
|
// Auxiliary files
|
|
for (const [name, file] of Object.entries(this.auxFiles)) {
|
|
await addFile(file, name);
|
|
}
|
|
|
|
// Resource files
|
|
if (this.templateFile) {
|
|
opts.template = this.templateFile.name;
|
|
await addFile(this.templateFile, this.templateFile.name);
|
|
}
|
|
if (this.referenceDoc) {
|
|
opts['reference-doc'] = this.referenceDoc.name;
|
|
await addFile(this.referenceDoc, this.referenceDoc.name);
|
|
}
|
|
if (this.bibFile) {
|
|
opts.bibliography = this.bibFile.name;
|
|
await addFile(this.bibFile, this.bibFile.name);
|
|
}
|
|
if (this.cslFile) {
|
|
opts.csl = this.cslFile.name;
|
|
await addFile(this.cslFile, this.cslFile.name);
|
|
}
|
|
if (this.abbrevFile) {
|
|
opts['citation-abbreviations'] = this.abbrevFile.name;
|
|
await addFile(this.abbrevFile, this.abbrevFile.name);
|
|
}
|
|
if (this.abbreviationsFile) {
|
|
opts.abbreviations = this.abbreviationsFile.name;
|
|
await addFile(this.abbreviationsFile, this.abbreviationsFile.name);
|
|
}
|
|
if (this.metadataFile) {
|
|
opts['metadata-file'] = this.metadataFile.name;
|
|
await addFile(this.metadataFile, this.metadataFile.name);
|
|
}
|
|
if (this.highlightThemeFile) {
|
|
opts['highlight-style'] = this.highlightThemeFile.name;
|
|
await addFile(this.highlightThemeFile, this.highlightThemeFile.name);
|
|
}
|
|
if (this.cssFiles.length > 0) {
|
|
opts.css = this.cssFiles.map(f => f.name);
|
|
for (const file of this.cssFiles) await addFile(file, file.name);
|
|
}
|
|
if (this.syntaxDefinitions.length > 0) {
|
|
opts['syntax-definition'] = this.syntaxDefinitions.map(f => f.name);
|
|
for (const file of this.syntaxDefinitions) await addFile(file, file.name);
|
|
}
|
|
if (this.headerFiles.length > 0) {
|
|
opts['include-in-header'] = this.headerFiles.map(f => f.name);
|
|
for (const file of this.headerFiles) await addFile(file, file.name);
|
|
}
|
|
if (this.beforeBodyFiles.length > 0) {
|
|
opts['include-before-body'] = this.beforeBodyFiles.map(f => f.name);
|
|
for (const file of this.beforeBodyFiles) await addFile(file, file.name);
|
|
}
|
|
if (this.afterBodyFiles.length > 0) {
|
|
opts['include-after-body'] = this.afterBodyFiles.map(f => f.name);
|
|
for (const file of this.afterBodyFiles) await addFile(file, file.name);
|
|
}
|
|
if (this.epubCoverImage) {
|
|
opts['epub-cover-image'] = this.epubCoverImage.name;
|
|
await addFile(this.epubCoverImage, this.epubCoverImage.name);
|
|
}
|
|
if (this.epubFonts.length > 0) {
|
|
opts['epub-fonts'] = this.epubFonts.map(f => f.name);
|
|
for (const file of this.epubFonts) await addFile(file, file.name);
|
|
}
|
|
if (this.luaFilters.length > 0) {
|
|
opts.filters = this.luaFilters.map(f => f.name);
|
|
for (const file of this.luaFilters) await addFile(file, file.name);
|
|
}
|
|
|
|
// Add options.json
|
|
zipFiles['options.json'] = fflate.strToU8(JSON.stringify(opts, null, 2));
|
|
|
|
// Create zip and download
|
|
const zipData = fflate.zipSync(zipFiles);
|
|
const blob = new Blob([zipData], { type: 'application/zip' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'example.zip';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
},
|
|
|
|
formatFileSize(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
},
|
|
|
|
downloadFile(file, name) {
|
|
const url = URL.createObjectURL(file);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = name;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
},
|
|
|
|
handleDrop(e) {
|
|
this.dragOver = false;
|
|
this.handleFiles(Array.from(e.dataTransfer.files));
|
|
},
|
|
|
|
handleFileInput(e) {
|
|
this.handleFiles(Array.from(e.target.files));
|
|
e.target.value = '';
|
|
},
|
|
|
|
handleFiles(fileList) {
|
|
fileList.forEach(file => {
|
|
if (!this.files[file.name]) {
|
|
this.fileOrder.push(file.name);
|
|
}
|
|
this.files[file.name] = file;
|
|
});
|
|
this.updateExtensions();
|
|
this.hasChanges = true;
|
|
},
|
|
|
|
removeFile(name) {
|
|
delete this.files[name];
|
|
this.fileOrder = this.fileOrder.filter(n => n !== name);
|
|
this.hasChanges = true;
|
|
},
|
|
|
|
onFileDragStart(idx, e) {
|
|
this.fileDraggingIdx = idx;
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
},
|
|
|
|
onFileDragOver(idx) {
|
|
this.fileDragOverIdx = idx;
|
|
},
|
|
|
|
onFileDrop(idx) {
|
|
this.fileDragOverIdx = -1;
|
|
if (this.fileDraggingIdx !== -1 && this.fileDraggingIdx !== idx) {
|
|
const draggedName = this.fileOrder[this.fileDraggingIdx];
|
|
this.fileOrder.splice(this.fileDraggingIdx, 1);
|
|
this.fileOrder.splice(idx, 0, draggedName);
|
|
this.hasChanges = true;
|
|
}
|
|
},
|
|
|
|
handleAuxFiles(e) {
|
|
Array.from(e.target.files).forEach(file => { this.auxFiles[file.name] = file; });
|
|
e.target.value = '';
|
|
this.hasChanges = true;
|
|
},
|
|
|
|
handleAuxDir(e) {
|
|
Array.from(e.target.files).forEach(file => {
|
|
const path = file.webkitRelativePath || file.name;
|
|
this.auxFiles[path] = file;
|
|
});
|
|
e.target.value = '';
|
|
this.hasChanges = true;
|
|
},
|
|
|
|
removeAuxFile(name) { delete this.auxFiles[name]; this.hasChanges = true; },
|
|
|
|
// Resource file handlers
|
|
handleBibFile(e) { if (e.target.files[0]) { this.bibFile = e.target.files[0]; this.hasChanges = true; } },
|
|
handleCslFile(e) { if (e.target.files[0]) { this.cslFile = e.target.files[0]; this.cslStyleName = ''; this.hasChanges = true; } },
|
|
handleAbbrevFile(e) { if (e.target.files[0]) { this.abbrevFile = e.target.files[0]; this.hasChanges = true; } },
|
|
handleAbbreviationsFile(e) { if (e.target.files[0]) { this.abbreviationsFile = e.target.files[0]; this.hasChanges = true; } },
|
|
handleHighlightTheme(e) { if (e.target.files[0]) { this.highlightThemeFile = e.target.files[0]; this.hasChanges = true; } },
|
|
handleTemplateFile(e) { if (e.target.files[0]) { this.templateFile = e.target.files[0]; this.hasChanges = true; } },
|
|
handleReferenceDoc(e) { if (e.target.files[0]) { this.referenceDoc = e.target.files[0]; this.hasChanges = true; } },
|
|
handleMetadataFile(e) { if (e.target.files[0]) { this.metadataFile = e.target.files[0]; this.hasChanges = true; } },
|
|
handleEpubCover(e) { if (e.target.files[0]) { this.epubCoverImage = e.target.files[0]; this.hasChanges = true; } },
|
|
handleEpubMetadata(e) { if (e.target.files[0]) { this.epubMetadataFile = e.target.files[0]; this.hasChanges = true; } },
|
|
|
|
// Generic file list handlers
|
|
addFilesToList(e, list) {
|
|
Array.from(e.target.files).forEach(file => {
|
|
if (!list.find(f => f.name === file.name)) list.push(file);
|
|
});
|
|
e.target.value = '';
|
|
this.hasChanges = true;
|
|
},
|
|
removeFileFromList(name, list) {
|
|
const idx = list.findIndex(f => f.name === name);
|
|
if (idx !== -1) list.splice(idx, 1);
|
|
this.hasChanges = true;
|
|
},
|
|
|
|
handleCssFiles(e) { this.addFilesToList(e, this.cssFiles); },
|
|
removeCssFile(name) { this.removeFileFromList(name, this.cssFiles); },
|
|
handleSyntaxDefs(e) { this.addFilesToList(e, this.syntaxDefinitions); },
|
|
removeSyntaxDef(name) { this.removeFileFromList(name, this.syntaxDefinitions); },
|
|
handleEpubFonts(e) { this.addFilesToList(e, this.epubFonts); },
|
|
removeEpubFont(name) { this.removeFileFromList(name, this.epubFonts); },
|
|
handleHeaderFiles(e) { this.addFilesToList(e, this.headerFiles); },
|
|
removeHeaderFile(name) { this.removeFileFromList(name, this.headerFiles); },
|
|
handleBeforeBodyFiles(e) { this.addFilesToList(e, this.beforeBodyFiles); },
|
|
removeBeforeBodyFile(name) { this.removeFileFromList(name, this.beforeBodyFiles); },
|
|
handleAfterBodyFiles(e) { this.addFilesToList(e, this.afterBodyFiles); },
|
|
removeAfterBodyFile(name) { this.removeFileFromList(name, this.afterBodyFiles); },
|
|
handleLuaFilters(e) { this.addFilesToList(e, this.luaFilters); },
|
|
removeLuaFilter(name) { this.removeFileFromList(name, this.luaFilters); },
|
|
|
|
onFilterDragStart(idx, e) {
|
|
this.filterDraggingIdx = idx;
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
},
|
|
|
|
onFilterDragOver(idx) {
|
|
this.filterDragOverIdx = idx;
|
|
},
|
|
|
|
onFilterDrop(idx) {
|
|
this.filterDragOverIdx = -1;
|
|
if (this.filterDraggingIdx !== -1 && this.filterDraggingIdx !== idx) {
|
|
const draggedFilter = this.luaFilters[this.filterDraggingIdx];
|
|
this.luaFilters.splice(this.filterDraggingIdx, 1);
|
|
this.luaFilters.splice(idx, 0, draggedFilter);
|
|
this.hasChanges = true;
|
|
}
|
|
},
|
|
|
|
// Metadata auto-add row
|
|
onMetadataInput() {
|
|
const last = this.customMetadata[this.customMetadata.length - 1];
|
|
if (last && (last.key || last.value)) {
|
|
this.customMetadata.push({ key: '', value: '' });
|
|
}
|
|
this.hasChanges = true;
|
|
},
|
|
removeMetadata(idx) {
|
|
this.customMetadata.splice(idx, 1);
|
|
if (this.customMetadata.length === 0) {
|
|
this.customMetadata.push({ key: '', value: '' });
|
|
}
|
|
this.hasChanges = true;
|
|
},
|
|
|
|
// Variables auto-add row
|
|
onVariableInput() {
|
|
const last = this.customVariables[this.customVariables.length - 1];
|
|
if (last && (last.key || last.value)) {
|
|
this.customVariables.push({ key: '', value: '' });
|
|
}
|
|
this.hasChanges = true;
|
|
},
|
|
removeVariable(idx) {
|
|
this.customVariables.splice(idx, 1);
|
|
if (this.customVariables.length === 0) {
|
|
this.customVariables.push({ key: '', value: '' });
|
|
}
|
|
this.hasChanges = true;
|
|
},
|
|
|
|
// CSL styles
|
|
async loadCslStyles() {
|
|
if (this.cslStylesLoaded) return;
|
|
try {
|
|
const response = await fetch('https://data.jsdelivr.com/v1/package/gh/citation-style-language/styles@master/flat');
|
|
if (!response.ok) throw new Error('Failed to fetch styles list');
|
|
const data = await response.json();
|
|
this.cslStylesList = data.files
|
|
.map(f => f.name)
|
|
.filter(name => name.endsWith('.csl') && name.lastIndexOf('/') === 0)
|
|
.map(name => name.slice(1, -4))
|
|
.sort();
|
|
this.cslStylesLoaded = true;
|
|
} catch (err) {
|
|
console.error('Failed to load CSL styles list:', err);
|
|
this.cslStylesList = ['apa', 'chicago-author-date', 'ieee', 'modern-language-association', 'vancouver', 'harvard-cite-them-right', 'nature', 'science'];
|
|
}
|
|
},
|
|
|
|
async fetchCslStyle() {
|
|
if (!this.cslStyleName.trim()) {
|
|
alert('Please enter or select a style name');
|
|
return;
|
|
}
|
|
try {
|
|
const url = `https://cdn.jsdelivr.net/gh/citation-style-language/styles@master/${this.cslStyleName}.csl`;
|
|
const response = await fetch(url);
|
|
if (!response.ok) throw new Error(`Style "${this.cslStyleName}" not found`);
|
|
const cslContent = await response.text();
|
|
const blob = new Blob([cslContent], { type: 'application/xml' });
|
|
this.cslFile = new File([blob], `${this.cslStyleName}.csl`, { type: 'application/xml' });
|
|
this.hasChanges = true;
|
|
} catch (err) {
|
|
alert(`Failed to fetch style: ${err.message}`);
|
|
}
|
|
},
|
|
|
|
// Extensions
|
|
async updateExtensions() {
|
|
this.inputExtensions = {};
|
|
this.outputExtensions = {};
|
|
this.inputExtensionsList = [];
|
|
this.outputExtensionsList = [];
|
|
|
|
if (this.inputFormat !== 'auto') {
|
|
try {
|
|
const extData = await window.pandocModule.query({ query: "extensions-for-format", format: this.inputFormat });
|
|
this.inputExtensionsList = Object.entries(extData)
|
|
.map(([name, defaultOn]) => ({ name, defaultOn }))
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
} catch (e) { console.warn('Could not get input extensions:', e); }
|
|
}
|
|
|
|
const outFmt = this.outputFormat === 'pdf' ? 'typst' : this.outputFormat;
|
|
if (outFmt !== 'auto') {
|
|
try {
|
|
const extData = await window.pandocModule.query({ query: "extensions-for-format", format: outFmt});
|
|
this.outputExtensionsList = Object.entries(extData)
|
|
.map(([name, defaultOn]) => ({ name, defaultOn }))
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
} catch (e) { console.warn('Could not get output extensions:', e); }
|
|
}
|
|
},
|
|
|
|
getExtensionChecked(direction, ext) {
|
|
const extState = direction === 'input' ? this.inputExtensions : this.outputExtensions;
|
|
if (extState[ext.name] !== undefined) return extState[ext.name];
|
|
return ext.defaultOn;
|
|
},
|
|
|
|
toggleExtension(direction, ext, e) {
|
|
const extState = direction === 'input' ? this.inputExtensions : this.outputExtensions;
|
|
if (e.target.checked) {
|
|
extState[ext.name] = true;
|
|
} else {
|
|
if (ext.defaultOn) {
|
|
extState[ext.name] = false;
|
|
} else {
|
|
delete extState[ext.name];
|
|
}
|
|
}
|
|
},
|
|
|
|
onInputFormatChange() { this.updateExtensions(); },
|
|
onOutputFormatChange() { this.updateExtensions(); },
|
|
|
|
buildFormatWithExtensions(baseFormat, extensions) {
|
|
if (!baseFormat || baseFormat === 'auto') return baseFormat;
|
|
let formatStr = baseFormat;
|
|
for (const [ext, enabled] of Object.entries(extensions)) {
|
|
if (enabled === true) formatStr += `+${ext}`;
|
|
else if (enabled === false) formatStr += `-${ext}`;
|
|
}
|
|
return formatStr;
|
|
},
|
|
|
|
buildOptions() {
|
|
const opts = {};
|
|
|
|
// Input format
|
|
if (this.inputFormat !== 'auto') {
|
|
opts.from = this.buildFormatWithExtensions(this.inputFormat, this.inputExtensions);
|
|
} else if (this.inputMode === 'file' && this.fileOrder.length > 0) {
|
|
const ext = this.fileOrder[0].split('.').pop().toLowerCase();
|
|
if (this.formatByExtension[ext]) {
|
|
opts.from = this.buildFormatWithExtensions(this.formatByExtension[ext], this.inputExtensions);
|
|
}
|
|
}
|
|
|
|
// Output format
|
|
if (this.outputFormat !== 'auto') {
|
|
opts.to = this.buildFormatWithExtensions(this.outputFormat, this.outputExtensions);
|
|
}
|
|
|
|
// Output file
|
|
let finalOutFile = this.outputFilename.trim() || this.outputFilenamePlaceholder;
|
|
if (!finalOutFile || finalOutFile === '(preview)') {
|
|
const effectiveFmt = this.effectiveOutputFormat || 'html';
|
|
const defaultExt = this.extensionByFormat[effectiveFmt] || 'html';
|
|
finalOutFile = `output.${defaultExt}`;
|
|
}
|
|
opts['output-file'] = finalOutFile;
|
|
this.outputFilenameActual = finalOutFile;
|
|
|
|
// Input files
|
|
if (this.inputMode === 'file') {
|
|
opts['input-files'] = this.fileOrder;
|
|
}
|
|
|
|
// General options
|
|
if (this.opts.standalone) opts.standalone = true;
|
|
if (this.opts.toc) {
|
|
opts['table-of-contents'] = true;
|
|
opts['toc-depth'] = parseInt(this.opts.tocDepth);
|
|
}
|
|
if (this.opts.numberSections) opts['number-sections'] = true;
|
|
if (this.opts.fileScope) opts['file-scope'] = true;
|
|
const shiftHeading = parseInt(this.opts.shiftHeading);
|
|
if (shiftHeading !== 0) opts['shift-heading-level-by'] = shiftHeading;
|
|
if (this.opts.preserveTabs) opts['preserve-tabs'] = true;
|
|
const tabStop = parseInt(this.opts.tabStop);
|
|
if (tabStop !== 4) opts['tab-stop'] = tabStop;
|
|
if (this.opts.eol) opts.eol = this.opts.eol;
|
|
if (this.opts.dpi) opts.dpi = parseInt(this.opts.dpi);
|
|
if (this.opts.numberOffset.trim()) {
|
|
opts['number-offset'] = this.opts.numberOffset.trim().split(',').map(n => parseInt(n.trim()) || 0);
|
|
}
|
|
if (this.opts.resourcePath.trim()) opts['resource-path'] = this.opts.resourcePath.trim();
|
|
if (this.opts.stripComments) opts['strip-comments'] = true;
|
|
if (this.opts.defaultImageExtension.trim()) opts['default-image-extension'] = this.opts.defaultImageExtension.trim();
|
|
if (this.abbreviationsFile) opts.abbreviations = this.abbreviationsFile.name;
|
|
|
|
// Track changes
|
|
if (this.opts.trackChanges) opts['track-changes'] = this.opts.trackChanges;
|
|
|
|
// Citations
|
|
if (this.opts.citationMethod === 'citeproc') opts.citeproc = true;
|
|
else if (this.opts.citationMethod === 'natbib') opts.natbib = true;
|
|
else if (this.opts.citationMethod === 'biblatex') opts.biblatex = true;
|
|
if (this.bibFile) opts.bibliography = this.bibFile.name;
|
|
if (this.opts.citationMethod === 'citeproc') {
|
|
if (this.cslFile) opts.csl = this.cslFile.name;
|
|
if (this.abbrevFile) opts['citation-abbreviations'] = this.abbrevFile.name;
|
|
}
|
|
|
|
// Math
|
|
if (this.opts.mathMethod) opts['html-math-method'] = this.opts.mathMethod;
|
|
|
|
// Code highlighting
|
|
if (this.opts.highlightStyle === 'custom' && this.highlightThemeFile) {
|
|
opts['highlight-style'] = this.highlightThemeFile.name;
|
|
} else if (this.opts.highlightStyle && this.opts.highlightStyle !== 'custom') {
|
|
opts['highlight-style'] = this.opts.highlightStyle;
|
|
}
|
|
if (this.syntaxDefinitions.length > 0) {
|
|
opts['syntax-definition'] = this.syntaxDefinitions.map(f => f.name);
|
|
}
|
|
if (this.opts.indentedCodeClasses.trim()) {
|
|
opts['indented-code-classes'] = this.opts.indentedCodeClasses.trim();
|
|
}
|
|
|
|
const baseOutFmt = this.effectiveOutputFormat;
|
|
|
|
// HTML options
|
|
if (this.htmlFormats.includes(baseOutFmt)) {
|
|
if (this.opts.selfContained) opts['embed-resources'] = true;
|
|
if (this.opts.htmlQTags) opts['html-q-tags'] = true;
|
|
if (this.opts.sectionDivs) opts['section-divs'] = true;
|
|
if (this.opts.titlePrefix.trim()) opts['title-prefix'] = this.opts.titlePrefix.trim();
|
|
if (this.opts.emailObfuscation) opts['email-obfuscation'] = this.opts.emailObfuscation;
|
|
if (this.opts.idPrefix.trim()) opts['id-prefix'] = this.opts.idPrefix.trim();
|
|
if (this.cssFiles.length > 0) opts.css = this.cssFiles.map(f => f.name);
|
|
}
|
|
|
|
// Markdown options
|
|
if (this.markdownFormats.includes(baseOutFmt)) {
|
|
if (this.opts.referenceLinks) opts['reference-links'] = true;
|
|
if (this.opts.referenceLocation) opts['reference-location'] = this.opts.referenceLocation;
|
|
if (this.opts.markdownHeadings) opts['markdown-headings'] = this.opts.markdownHeadings;
|
|
}
|
|
|
|
// RST options
|
|
if (baseOutFmt === 'rst') {
|
|
if (this.opts.referenceLinksRst) opts['reference-links'] = true;
|
|
if (this.opts.listTables) opts['list-tables'] = true;
|
|
}
|
|
|
|
// Caption position
|
|
if (this.captionPositionFormats.includes(baseOutFmt)) {
|
|
if (this.opts.figureCaptionPosition) opts['figure-caption-position'] = this.opts.figureCaptionPosition;
|
|
if (this.opts.tableCaptionPosition) opts['table-caption-position'] = this.opts.tableCaptionPosition;
|
|
}
|
|
|
|
// ASCII
|
|
if (this.asciiFormats.includes(baseOutFmt)) {
|
|
const asciiChecked = this.htmlFormats.includes(baseOutFmt) ? this.opts.asciiHtml : this.opts.ascii;
|
|
if (asciiChecked) opts.ascii = true;
|
|
}
|
|
|
|
// List of figures/tables
|
|
if (this.listOfFormats.includes(baseOutFmt)) {
|
|
if (this.opts.lof) opts['list-of-figures'] = true;
|
|
if (this.opts.lot) opts['list-of-tables'] = true;
|
|
}
|
|
|
|
// Top-level division
|
|
if (this.topLevelDivisionFormats.includes(baseOutFmt) && this.opts.topLevelDivision) {
|
|
opts['top-level-division'] = this.opts.topLevelDivision;
|
|
}
|
|
|
|
// EPUB options
|
|
if (['epub', 'epub2', 'epub3'].includes(baseOutFmt)) {
|
|
if (this.epubCoverImage) opts['epub-cover-image'] = this.epubCoverImage.name;
|
|
if (this.epubMetadataFile) opts['epub-metadata'] = this.epubMetadataFile.name;
|
|
if (this.epubFonts.length > 0) opts['epub-fonts'] = this.epubFonts.map(f => f.name);
|
|
if (!this.opts.epubTitlePage) opts['epub-title-page'] = false;
|
|
if (this.opts.epubSubdirectory.trim()) opts['epub-subdirectory'] = this.opts.epubSubdirectory.trim();
|
|
}
|
|
|
|
// ODT options
|
|
if (baseOutFmt === 'odt') {
|
|
if (this.opts.linkImages) opts['link-images'] = true;
|
|
}
|
|
|
|
// Template
|
|
if (this.templateFile) opts.template = this.templateFile.name;
|
|
|
|
// Include files
|
|
if (this.headerFiles.length > 0) opts['include-in-header'] = this.headerFiles.map(f => f.name);
|
|
if (this.beforeBodyFiles.length > 0) opts['include-before-body'] = this.beforeBodyFiles.map(f => f.name);
|
|
if (this.afterBodyFiles.length > 0) opts['include-after-body'] = this.afterBodyFiles.map(f => f.name);
|
|
|
|
// Variables
|
|
const variables = {};
|
|
this.customVariables.forEach(v => {
|
|
if (v.key.trim() && v.value.trim()) variables[v.key.trim()] = v.value.trim();
|
|
});
|
|
if (Object.keys(variables).length > 0) opts.variables = variables;
|
|
|
|
// Chunked HTML
|
|
if (baseOutFmt === 'chunkedhtml') {
|
|
if (this.opts.splitLevel) opts['split-level'] = parseInt(this.opts.splitLevel);
|
|
if (this.opts.chunkTemplate.trim()) opts['chunk-template'] = this.opts.chunkTemplate.trim();
|
|
}
|
|
|
|
// Notebook
|
|
if (baseOutFmt === 'ipynb' && this.opts.ipynbOutput) {
|
|
opts['ipynb-output'] = this.opts.ipynbOutput;
|
|
}
|
|
|
|
// Reference doc
|
|
if (this.docFormats.includes(baseOutFmt) && this.referenceDoc) {
|
|
opts['reference-doc'] = this.referenceDoc.name;
|
|
}
|
|
|
|
// Slides
|
|
if (this.slideFormats.includes(baseOutFmt)) {
|
|
if (this.opts.slideLevel) opts['slide-level'] = parseInt(this.opts.slideLevel);
|
|
if (this.opts.incremental) opts.incremental = true;
|
|
}
|
|
|
|
// Metadata file
|
|
if (this.metadataFile) opts['metadata-file'] = this.metadataFile.name;
|
|
|
|
// Metadata
|
|
const metadata = {};
|
|
this.customMetadata.forEach(m => {
|
|
if (m.key.trim() && m.value.trim()) {
|
|
const key = m.key.trim();
|
|
const value = m.value.trim();
|
|
// author should be an array
|
|
if (key === 'author') {
|
|
metadata[key] = [value];
|
|
} else {
|
|
metadata[key] = value;
|
|
}
|
|
}
|
|
});
|
|
if (Object.keys(metadata).length > 0) opts.metadata = metadata;
|
|
|
|
// Wrapping
|
|
if (this.opts.wrap !== 'auto') opts.wrap = this.opts.wrap;
|
|
if (this.opts.columns.trim()) opts.columns = parseInt(this.opts.columns);
|
|
|
|
// Extract media
|
|
if (this.opts.extractMedia) opts['extract-media'] = 'media.zip';
|
|
|
|
// Lua filters
|
|
if (this.luaFilters.length > 0) {
|
|
opts.filters = this.luaFilters.map(f => f.name);
|
|
}
|
|
|
|
return opts;
|
|
},
|
|
|
|
async convert() {
|
|
// Ensure pandoc is loaded before converting
|
|
await this.ensurePandocLoaded();
|
|
|
|
this.isConverting = true;
|
|
this.showOutput = false;
|
|
this.messages = [];
|
|
this.output = null;
|
|
this.outputPreview = '';
|
|
this.mediaZip = null;
|
|
|
|
try {
|
|
const options = this.buildOptions();
|
|
|
|
// Build files object
|
|
const files = { ...this.files, ...this.auxFiles };
|
|
if (this.bibFile) files[this.bibFile.name] = this.bibFile;
|
|
if (this.cslFile) files[this.cslFile.name] = this.cslFile;
|
|
if (this.abbrevFile) files[this.abbrevFile.name] = this.abbrevFile;
|
|
if (this.abbreviationsFile) files[this.abbreviationsFile.name] = this.abbreviationsFile;
|
|
this.cssFiles.forEach(file => { files[file.name] = file; });
|
|
if (this.highlightThemeFile) files[this.highlightThemeFile.name] = this.highlightThemeFile;
|
|
this.syntaxDefinitions.forEach(file => { files[file.name] = file; });
|
|
if (this.templateFile) files[this.templateFile.name] = this.templateFile;
|
|
if (this.referenceDoc) files[this.referenceDoc.name] = this.referenceDoc;
|
|
if (this.metadataFile) files[this.metadataFile.name] = this.metadataFile;
|
|
if (this.epubCoverImage) files[this.epubCoverImage.name] = this.epubCoverImage;
|
|
if (this.epubMetadataFile) files[this.epubMetadataFile.name] = this.epubMetadataFile;
|
|
this.epubFonts.forEach(font => { files[font.name] = font; });
|
|
this.headerFiles.forEach(file => { files[file.name] = file; });
|
|
this.beforeBodyFiles.forEach(file => { files[file.name] = file; });
|
|
this.afterBodyFiles.forEach(file => { files[file.name] = file; });
|
|
this.luaFilters.forEach(file => { files[file.name] = file; });
|
|
|
|
const stdin = this.inputMode === 'text' ? this.textInput : null;
|
|
const isPdfTypst = this.outputFormat === 'pdf';
|
|
|
|
if (isPdfTypst) {
|
|
await window.loadTypst();
|
|
const typstOptions = { ...options };
|
|
typstOptions.to = this.buildFormatWithExtensions('typst', this.outputExtensions);
|
|
typstOptions.standalone = true;
|
|
delete typstOptions['output-file'];
|
|
|
|
// Auto-inject embed_images filter for PDF via Typst.
|
|
// Otherwise conversion fails because we can't write the images in a
|
|
// temporary directory in the WASM sandbox. See jgm/pandoc#11584.
|
|
const embedImagesLua = `function Pandoc (doc)
|
|
doc = pandoc.mediabag.fill(doc)
|
|
return doc:walk {
|
|
Image = function (img)
|
|
local mimetype, contents = pandoc.mediabag.lookup(img.src)
|
|
if mimetype and contents then
|
|
img.src = pandoc.mediabag.make_data_uri(mimetype, contents)
|
|
return img
|
|
end
|
|
end
|
|
}
|
|
end
|
|
`;
|
|
const embedImagesFile = new File(
|
|
[new Blob([embedImagesLua], { type: 'text/plain' })],
|
|
'_embed_images.lua'
|
|
);
|
|
files['_embed_images.lua'] = embedImagesFile;
|
|
const existingFilters = typstOptions.filters || [];
|
|
typstOptions.filters = ['_embed_images.lua', ...existingFilters];
|
|
|
|
const typstResult = await window.pandocModule.convert(typstOptions, stdin, files);
|
|
if (typstResult.stderr && typstResult.stderr.includes('ERROR')) {
|
|
throw new Error(typstResult.stderr);
|
|
}
|
|
|
|
const typstContent = typstResult.stdout;
|
|
let pdfData;
|
|
try {
|
|
$typst.resetShadow();
|
|
for (const [path, file] of Object.entries(files)) {
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
const uint8Array = new Uint8Array(arrayBuffer);
|
|
$typst.mapShadow('/' + path, uint8Array);
|
|
if (!path.startsWith('/')) $typst.mapShadow(path, uint8Array);
|
|
}
|
|
const mainTypstPath = '/main.typ';
|
|
const typstBytes = new TextEncoder().encode(typstContent);
|
|
$typst.mapShadow(mainTypstPath, typstBytes);
|
|
pdfData = await $typst.pdf({ mainFilePath: mainTypstPath });
|
|
} catch (typstError) {
|
|
const errorStr = String(typstError);
|
|
const messageMatch = errorStr.match(/message:\s*"([^"]+)"/);
|
|
let errorMsg = messageMatch ? messageMatch[1] : errorStr;
|
|
throw new Error(`Typst: ${errorMsg}`);
|
|
}
|
|
|
|
if (!pdfData || pdfData.length === 0) {
|
|
throw new Error('Typst produced empty PDF output');
|
|
}
|
|
|
|
const pdfBlob = new Blob([pdfData], { type: 'application/pdf' });
|
|
this.output = pdfBlob;
|
|
this.displayResults(typstResult, options, files);
|
|
} else {
|
|
const result = await window.pandocModule.convert(options, stdin, files);
|
|
this.displayResults(result, options, files);
|
|
}
|
|
} catch (err) {
|
|
this.showOutput = true;
|
|
this.messages.push({ type: 'error', text: `Conversion failed: ${err.message}` });
|
|
} finally {
|
|
this.isConverting = false;
|
|
}
|
|
},
|
|
|
|
displayResults(result, options, files) {
|
|
this.showOutput = true;
|
|
|
|
if (result.warnings && result.warnings.length > 0) {
|
|
result.warnings.forEach(w => {
|
|
const msgType = w.verbosity === 'INFO' ? 'info' : 'warning';
|
|
this.messages.push({ type: msgType, text: w.pretty || w.message || JSON.stringify(w) });
|
|
});
|
|
}
|
|
|
|
if (result.stderr && result.stderr.includes('ERROR')) {
|
|
this.messages.push({ type: 'error', text: result.stderr });
|
|
return;
|
|
}
|
|
|
|
const isBinary = this.binaryFormats.includes(options.to);
|
|
|
|
if (this.output) {
|
|
// Already set (e.g., pdf via typst)
|
|
} else if (options['output-file'] && files[options['output-file']]) {
|
|
this.output = files[options['output-file']];
|
|
if (!isBinary) {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
this.outputPreview = reader.result.substring(0, 50000);
|
|
if (reader.result.length > 50000) {
|
|
this.outputPreview += '\n\n... (truncated)';
|
|
}
|
|
};
|
|
reader.readAsText(this.output);
|
|
}
|
|
} else if (result.stdout) {
|
|
this.output = new Blob([result.stdout], { type: 'text/plain' });
|
|
if (!isBinary) {
|
|
this.outputPreview = result.stdout;
|
|
}
|
|
}
|
|
|
|
// Check for extracted media
|
|
if (files['media.zip'] && files['media.zip'].size > 0) {
|
|
this.mediaZip = files['media.zip'];
|
|
}
|
|
|
|
// Disable convert button until next change
|
|
this.hasChanges = false;
|
|
},
|
|
|
|
download() {
|
|
if (!this.output) return;
|
|
const url = URL.createObjectURL(this.output);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = this.outputFilenameActual || 'output';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
},
|
|
|
|
downloadMedia() {
|
|
if (!this.mediaZip) return;
|
|
const url = URL.createObjectURL(this.mediaZip);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'media.zip';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
},
|
|
|
|
async copyToClipboard() {
|
|
try {
|
|
await navigator.clipboard.writeText(this.outputPreview);
|
|
this.copyBtnText = '✓ Copied!';
|
|
setTimeout(() => { this.copyBtnText = '📋 Copy to Clipboard'; }, 2000);
|
|
} catch (err) {
|
|
this.messages.push({ type: 'error', text: 'Failed to copy to clipboard' });
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
// Mount Petite Vue after module loads (pandoc.wasm is ready)
|
|
// Using mount() without selector scans DOM for v-scope attributes
|
|
PetiteVue.createApp().mount();
|