Skip to content

Compile Step: Convert to ePub and/or DOCX #304

@pdworkman

Description

@pdworkman

I have forked Dan Hanly's script which converts the compiled MD into ePub and DOCX formats, so that the user doesn't have to manually run the Pandoc plugin to do so. Dan's script included using a CSS file to style the ePub file. I have added in the ability to use a Pandoc Word Template to style the converted DOCX file.

The script will look for a pandoc docx template in the pandoc file where the css file is stored, or alternatively from another location the user specifies.

It is just a Word file with the name custom-reference.docx by default. Create a Word file with the Heading Styles, header, and footer (and whatever else you like). No more editing the Word file after compiling to get rid of the stupid blue headings Word defaults to!

const spawnSync = require('child_process').spawnSync;
const path = require('node:path');

async function compile(input, context) {
  // This is undocumented and could break.
  let basePath = context.app.vault.adapter.basePath;
  if (!basePath.endsWith("/")) {
    basePath = basePath + "/";
  }

  let projectPath = context.projectPath;
  if (!projectPath.endsWith("/")) {
    projectPath = projectPath + "/";
  }

  let userScriptPath = context.app?.plugins?.plugins?.longform?.cachedSettings?.userScriptFolder;
  if (!userScriptPath.endsWith("/")) {
    userScriptPath = userScriptPath + "/";
  }

  let epubMetadataPath = context.optionValues["epubMetadataPath"].trim();
  if (!epubMetadataPath) {
    epubMetadataPath = 'meta.yml';
  }

  const metadataPath = path.join(basePath, projectPath, epubMetadataPath);

  let epubCssPath = context.optionValues["epubCssPath"].trim();
  if (!epubCssPath) {
     epubCssPath = path.join(basePath, userScriptPath, 'pandoc', 'epub.css');
  }
  
  // Resolve the CSS path to absolute path if it's relative
  if (epubCssPath && !path.isAbsolute(epubCssPath)) {
    epubCssPath = path.join(basePath, epubCssPath);
  }

  let docxTemplatePath = context.optionValues["docxTemplatePath"].trim();
  if (!docxTemplatePath) {
    docxTemplatePath = path.join(basePath, userScriptPath, 'pandoc', 'custom-reference.docx');
  }
  
  // Resolve the DOCX template path to absolute path if provided
  if (docxTemplatePath && !path.isAbsolute(docxTemplatePath)) {
    docxTemplatePath = path.join(basePath, docxTemplatePath);
  }

  const pandocPath = context.optionValues["pandocPath"].trim();

  const compileAsDocx = context.optionValues["docx"];
  const compileAsEpub = context.optionValues["epub"];

  const app = context.app;
  const projectFolder = app.vault.getFolderByPath(context.projectPath);
  const projectFiles = projectFolder.children.filter((file) => file.extension === "md");
  const lastModifiedFile = projectFiles.sort((a, b) => b.stat.mtime - a.stat.mtime)[0];
  const manuscriptPath = path.join(basePath, lastModifiedFile.path);

  const outputFileName = lastModifiedFile.path.replace(/\.[^/.]+$/, "");
  const outputFilePath = path.join(basePath, outputFileName);

  const tocArgs = context.optionValues["toc"] ? ['--toc',  '--toc-depth=1', '-M toc-title="Table of Contents"', '-V toc-title="Table of Contents"'] : [];

  const processExport = () => {
    if (compileAsDocx) {
      const compiledFilePath = outputFilePath + ".docx";
      console.log('Compiling DocX File at "' + compiledFilePath + '"');
      
      // Build the pandoc arguments array
      const docxArgs = [
        `"${manuscriptPath}"`,
        '--from=markdown',
        `-o "${compiledFilePath}"`,
        '--to=docx'
      ];

      // Add template argument - check if template exists first
      if (docxTemplatePath) {
        const fs = require('fs');
        if (fs.existsSync(docxTemplatePath)) {
          docxArgs.push(`--reference-doc="${docxTemplatePath}"`);
          console.log('Using DOCX template at "' + docxTemplatePath + '"');
        } else {
          console.log('Warning: DOCX template not found at "' + docxTemplatePath + '". Falling back to CSS styling.');
          docxArgs.push(`--css="${epubCssPath}"`);
          console.log('Using CSS at "' + epubCssPath + '"');
        }
      } else {
        // This shouldn't happen since we now set a default, but keeping as fallback
        docxArgs.push(`--css="${epubCssPath}"`);
        console.log('Using CSS at "' + epubCssPath + '"');
      }

      // Add table of contents and standalone flags
      docxArgs.push(...tocArgs, '-s');

      const docxResult = spawnSync(`"${pandocPath}"`, docxArgs, {encoding: "utf-8", shell: true});
      
      if (docxResult.status !== 0) {
        console.log(docxResult.stderr)
        console.log(docxResult.stdout)
      }
    }

    if (compileAsEpub) {
      const compiledFilePath = outputFilePath + ".epub";
      console.log('Compiling EPUB File at "' + compiledFilePath + '" using metadata at "' + metadataPath + '" and css at "' + epubCssPath + '"');
      
      let metadataArg = "--metadata-file=";
      if (metadataPath.includes('.xml')) {
        metadataArg = "--epub-metadata=";
      }

      const epubArgs = [
        `"${manuscriptPath}"`,
        '--from=markdown',
        `-o "${compiledFilePath}"`,
        '--to=epub',
        `${metadataArg}"${metadataPath}"`,
        ...tocArgs,
        '-s'
      ];

      // Add CSS only if the file exists
      if (epubCssPath) {
        const fs = require('fs');
        if (fs.existsSync(epubCssPath)) {
          epubArgs.push(`--css="${epubCssPath}"`);
          console.log('Using CSS at "' + epubCssPath + '"');
        } else {
          console.log('Warning: CSS file not found at "' + epubCssPath + '". Proceeding without CSS.');
        }
      }

      const epubResult = spawnSync(`"${pandocPath}"`, epubArgs, {encoding: "utf-8", shell: true});

      if (epubResult.status !== 0) {
        console.log(epubResult.stderr)
        console.log(epubResult.stdout)
      }
    }
  };

  await processExport();
}

module.exports = {
  description: {
    name: "Pandoc Export",
    description: "Exports manuscript using Pandoc",
    availableKinds: ["Manuscript"],
    options: [
      {
        id: "pandocPath",
        name: "Direct Path of Pandoc",
        description: "Run '$ which pandoc' in Mac/Linux or '$ Get-Command pandoc' in Windows Powershell to discover the direct path.",
        type: "Text",
        default: "",
      },
      {
        id: "epubCssPath",
        name: "CSS File Path specifically for the .epub exporter",
        description: "You can provide your own, or trust the one packaged with this script",
        type: "Text",
        default: "",
      },
      {
        id: "docxTemplatePath",
        name: "DOCX Template File Path",
        description: "Path to a .docx file to use as a reference template for styling. Leave blank to use the default template at pandoc/custom-reference.docx, or enter a custom path.",
        type: "Text",
        default: "",
      },
      {
        id: "epubMetadataPath",
        name: "Location of your epub metadata file.",
        description: "Leave this blank to look for meta.yml in the project index folder. ePubs without metadata will often not be accepted by major ebook retailers. Can be either a .yml file or in an .xml file, adhering to the Dublic Core standards.",
        type: "Text",
        default: "",
      },
      {
        id: "toc",
        name: "Include a Table of Contents?",
        description: "Table of Contents will be automatically generated. For .docx files, the table of contents must be manually refreshed on the first open of the file.",
        type: "Boolean",
        default: true,
      },
      {
        id: "docx",
        name: "As a .docx file",
        description: "Export as a Microsoft Word .docx File",
        type: "Boolean",
        default: true,
      },
      {
        id: "epub",
        name: "As an .epub file",
        description: "Export as an EPUB File",
        type: "Boolean",
        default: true,
      },
    ],
  },
  compile:  compile,
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    compilePertaining to the Compile feature.enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions