├── .eleventy.js ├── .gitignore ├── CHANGELOG.md ├── README.md ├── package.json └── test ├── sample-data-cascade ├── _includes │ └── layouts │ │ └── docx.njk ├── index.docx ├── second-page.11tydata.js └── second-page.docx └── sample ├── _includes └── layouts │ └── docx.njk └── index.docx /.eleventy.js: -------------------------------------------------------------------------------- 1 | const mammoth = require("mammoth"); 2 | const cheerio = require("cheerio"); 3 | const lodashMerge = require("lodash.merge"); 4 | const path = require("path"); 5 | const fs = require("fs"); 6 | const md5 = require("js-md5"); 7 | const fsPromises = require("fs/promises"); 8 | 9 | module.exports = function (eleventyConfig, suppliedOptions) { 10 | const defaultOptions = { 11 | layout: "layouts/docx.njk", 12 | outputDir: eleventyConfig.dir?.output || "_site", 13 | imageDir: "images", 14 | useGlobalLayout: true, 15 | cheerioTransform: null, 16 | mammothConfig: {}, 17 | }; 18 | // Return your Object options: 19 | let options = lodashMerge({}, defaultOptions, suppliedOptions); 20 | eleventyConfig.addTemplateFormats("docx"); 21 | // ignore temporary files 22 | eleventyConfig.ignores.add("**/~*.docx"); 23 | eleventyConfig.addExtension("docx", { 24 | getData: function () { 25 | if (options.useGlobalLayout) { 26 | // Set layout for all docx files 27 | return { 28 | layout: options.layout, 29 | }; 30 | } else { 31 | // The layout must be set elsewhere in the data cascade (eg. directory data files) 32 | return {}; 33 | } 34 | }, 35 | compile: function (str, inputPath) { 36 | return async (data) => { 37 | // This checks the data cascade for specific mammothConfig and cheerioTransform 38 | // If they're not in the cascade, we use the global options (set when adding plugin to .eleventy.js) 39 | const mammothConfig = data.mammothConfig 40 | ? data.mammothConfig 41 | : options.mammothConfig; 42 | const cheerioTransform = data.cheerioTransform 43 | ? data.cheerioTransform 44 | : options.cheerioTransform; 45 | 46 | const imageSubfolderName = data.page.fileSlug; 47 | const imageNamePrefix = data.page.fileSlug; 48 | 49 | var imageCounter = 1; 50 | var imageSrcByHash = {}; 51 | 52 | mammothConfig.convertImage = mammoth.images.imgElement(async function ( 53 | image 54 | ) { 55 | const imageBuffer = await image.read(); 56 | const imageHash = md5(imageBuffer); 57 | // check if image hash is already in our saved hashes. 58 | // if it is, we return the saved src 59 | if (imageSrcByHash[imageHash]) { 60 | return { 61 | src: imageSrcByHash[imageHash], 62 | }; 63 | } 64 | 65 | // no saved image hash - write new image file 66 | const imageExt = image.contentType.split("/").pop(); 67 | const imageDir = path.join( 68 | options.outputDir, 69 | imageSubfolderName, 70 | options.imageDir 71 | ); 72 | 73 | if (!fs.existsSync(imageDir)) { 74 | await fsPromises.mkdir(imageDir, { recursive: true }); 75 | } 76 | 77 | const imageFilename = `${ 78 | imageNamePrefix ? imageNamePrefix + "_" : "" 79 | }image${imageCounter}.${imageExt}`; 80 | const imageFilePath = path.join(imageDir, imageFilename); 81 | fs.writeFile(imageFilePath, imageBuffer, "base64", function (err) { 82 | if (err) { 83 | console.log(err); 84 | } else { 85 | console.log(`Saved image file: ${imageFilePath}`); 86 | } 87 | }); 88 | 89 | imageCounter++; 90 | // save this src and hash to our saved hashes 91 | const src = `${options.imageDir}/${imageFilename}`; 92 | imageSrcByHash[imageHash] = src; 93 | return { 94 | src, 95 | }; 96 | }); 97 | const rawHtml = await mammoth 98 | .convertToHtml({ path: inputPath }, mammothConfig) 99 | .then((result) => result.value); 100 | if (typeof cheerioTransform === "function") { 101 | const $ = cheerio.load(rawHtml); 102 | cheerioTransform($); 103 | return $("body").html(); 104 | } else { 105 | return rawHtml; 106 | } 107 | }; 108 | }, 109 | }); 110 | }; 111 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | node_modules 3 | _site 4 | package-lock.json -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG.md 2 | 3 | ## 1.0.3 (2022-04-04) 4 | 5 | Features: 6 | - automatically ignores `.docx` files that start with `~` - these are temporary files that only exist when you have the Word document open 7 | - default behaviour now saves image files into subfolders specific for each document - eg. a document with the filename `mydoc.docx` will save images into `/images/mydoc/` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eleventy-plugin-docx 2 | 3 | This Eleventy plugin adds a [custom file extension handler](https://github.com/11ty/eleventy/issues/117) for Microsoft Word `.docx` documents. 4 | 5 | ## What it does 6 | 7 | This plugin uses the [mammoth.js](https://github.com/mwilliamson/mammoth.js/) library to convert docx files to HTML. 8 | 9 | It can also be configured with a custom transformer function using [cheerio](https://cheerio.js.org/). `cheerio` lets you use a jQuery-like syntax to adjust the HTML. 10 | 11 | ## Compatibility 12 | 13 | This is compatible with Eleventy 1.0.0 beta 8 and newer. 14 | 15 | ## Installation 16 | 17 | Install using npm: 18 | 19 | ```bash 20 | npm i eleventy-plugin-docx 21 | ``` 22 | 23 | ## Usage 24 | 25 | Add it to your Eleventy config file (`.eleventy.js`): 26 | 27 | ```js 28 | const DocxPlugin = require('eleventy-plugin-docx'); 29 | 30 | module.exports = function(eleventyConfig) { 31 | // Use default options 32 | eleventyConfig.addPlugin(DocxPlugin) 33 | }; 34 | ``` 35 | 36 | ### Working with layouts 37 | 38 | By default, the plugin will try to use `layouts/docx.njk` as the layout for all `.docx` files in the Eleventy site's input directory. 39 | 40 | The docx content is rendered in the template using `{{content|safe}}`. 41 | 42 | You can: 43 | - change the global layout path by setting the `layout` option in the [configuration options](#configuration-options) 44 | - [use different layouts for different directories by using directory data files](#overriding-configuration-with-directory-data-files) 45 | 46 | ### Configuration options 47 | Configuration options can be included as an object when you add the plugin to `.eleventy.js`: 48 | 49 | ```js 50 | const DocxPlugin = require('eleventy-plugin-docx'); 51 | 52 | module.exports = function(eleventyConfig) { 53 | // Customise configuration options 54 | eleventyConfig.addPlugin(DocxPlugin, { 55 | // Layout path for docx files, relative to 'includes' directory 56 | layout: 'layouts/docx.njk', 57 | 58 | // Where to use the layout above for all docx files 59 | // If this is set to false, you must set the layout in the data cascade (see below for details) 60 | useGlobalLayout: true, 61 | 62 | // Configuration object that gets passed through to mammoth.js 63 | // See documentation: https://github.com/mwilliamson/mammoth.js/#api 64 | mammothConfig: { 65 | styleMap: [ 66 | "p[style-name='Quote'] => blockquote" 67 | ] 68 | }, 69 | 70 | // Transformer function that gives you cheerio's $ function to adjust Mammoth's output 71 | // You don't need to return anything - this is handled by the plugin 72 | // See cheerio docs for more info: https://cheerio.js.org/ 73 | cheerioTransform: ($) => { 74 | 75 | // Add IDs to each subheading 76 | $('h2').each((index, h2Tag) => { 77 | $(h2Tag).attr('id', `section-${index + 1}`) 78 | }) 79 | 80 | // Add alt="" to img tags without alt attribute 81 | $('img:not([alt])').attr('alt', ''); 82 | 83 | // Remove manual line breaks 84 | $('br').remove(); 85 | 86 | }, 87 | 88 | }) 89 | }; 90 | ``` 91 | 92 | ## Overriding configuration with directory data files 93 | 94 | The configuration you set when you add the plugin to `.eleventy.js` will be used by default for all `.docx` files. 95 | 96 | If you want to set specific configuration options for different documents, you can override these options in [directory data files](https://www.11ty.dev/docs/data-template-dir/). 97 | 98 | For example, you might have content set up like this: 99 | 100 | ``` 101 | src/ 102 | ├── index.docx 103 | └── second-page/ 104 | ├── index.docx 105 | └── second-page.11tydata.js 106 | ``` 107 | 108 | In this case, you could set your configuration in `second-page.11tydata.js` and it would only apply to the documents in that directory and subdirectories: 109 | ```js 110 | // src/second-page/second-page.11tydata.js 111 | module.exports = { 112 | mammothConfig: { 113 | styleMap: [ 114 | "p[style-name='Heading 1'] => h1.heading" 115 | ] 116 | }, 117 | cheerioTransform: ($) => { 118 | $('h1').attr('data-cheerio', 'true'); 119 | }, 120 | } 121 | ``` 122 | 123 | ### Overriding the global layout setting 124 | 125 | At the moment, it's not possible to set a default layout, and then override the default layout in directory data files, like you can for `mammothConfig` and `cheerioTransform` ([see above](#overriding-configuration-with-directory-data-files)). 126 | 127 | If you want to set different layouts, you need to: 128 | - set `useGlobalLayout` to `false` when adding the plugin to `.eleventy.js` 129 | - make sure you set `layout` in directory data files: 130 | ```js 131 | // Directory data file (eg. about-page.11tydata.js) 132 | module.exports = { 133 | layout: 'layouts/about-page.njk' 134 | } 135 | ``` 136 | 137 | ## Using with `eleventy-plugin-render` 138 | 139 | This plugin pairs nicely with the new `eleventy-plugin-render`, which gives you a shortcode to render files inside templates. 140 | 141 | This means you can render Word document content within your other content. 142 | 143 | To use with `eleventy-plugin-render`: 144 | 1. Make sure you're using Eleventy 1.0.0 beta 7 or newer. 145 | 2. add `eleventy-plugin-render` to your Eleventy config by following [the plugin's installation instructions](https://www.11ty.dev/docs/plugins/render/). 146 | 3. use the `renderFile` shortcode wherever you want (in Markdown files, Nunjucks templates etc.): 147 | 148 | ``` 149 | {% renderFile './src/word-document.docx' %} 150 | ``` 151 | 152 | Note: the file path is relative to the project root folder. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eleventy-plugin-docx", 3 | "version": "1.0.4", 4 | "description": "Use Word documents as Eleventy input", 5 | "main": ".eleventy.js", 6 | "scripts": { 7 | "sample": "npx @11ty/eleventy --input=test/sample", 8 | "sample-data-casade": "npx @11ty/eleventy --input=test/sample-data-cascade" 9 | }, 10 | "publishConfig": { 11 | "access": "public", 12 | "registry": "https://registry.npmjs.org/" 13 | }, 14 | "keywords": [ 15 | "eleventy", 16 | "eleventy-plugin", 17 | "docx" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/larryhudson/eleventy-plugin-docx.git" 22 | }, 23 | "author": { 24 | "name": "Larry Hudson", 25 | "email": "larryhudson@hey.com", 26 | "url": "https://www.larryhudson.io" 27 | }, 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/larryhudson/eleventy-plugin-docx/issues" 31 | }, 32 | "homepage": "https://github.com/larryhudson/eleventy-plugin-docx#readme", 33 | "dependencies": { 34 | "@11ty/eleventy": "^1.0.0-beta.8", 35 | "cheerio": "^1.0.0-rc.10", 36 | "js-md5": "^0.7.3", 37 | "lodash.merge": "^4.6.2", 38 | "mammoth": "^1.4.19" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/sample-data-cascade/_includes/layouts/docx.njk: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |