1const fs = require("fs").promises; 2const marked = require("marked"); 3const jsdom = require("jsdom"); 4const { JSDOM } = jsdom; 5const path = require("path"); 6 7// Setup some options for our markdown renderer 8marked.setOptions({ 9 renderer: new marked.Renderer(), 10 11 // Add a code highlighter 12 highlight: function (code, forlanguage) { 13 const hljs = require("highlight.js"); 14 language = hljs.getLanguage(forlanguage) ? forlanguage : "plaintext"; 15 return hljs.highlight(code, { language }).value; 16 }, 17 pedantic: false, 18 gfm: true, 19 breaks: false, 20 sanitize: false, 21 smartLists: true, 22 smartypants: false, 23 xhtml: false, 24}); 25 26/** 27 * Read the input .md file, and write to a corresponding .html file 28 * @param {string} infile path to input file 29 * @returns {Promise<string>} name of output file (for status update) 30 */ 31async function renderit(infile) { 32 console.log(`Reading ${infile}`); 33 basename = path.basename(infile, ".md"); 34 const outfile = path.join(path.dirname(infile), `${basename}.html`); 35 let f1 = await fs.readFile(infile, "utf-8"); 36 37 // oh the irony of removing a BOM before posting to unicode.org 38 if (f1.charCodeAt(0) == 0xfeff) { 39 f1 = f1.substring(3); 40 } 41 42 // render to HTML 43 const rawHtml = marked(f1); 44 45 // now fix. Spin up a JSDOM so we can manipulate 46 const dom = new JSDOM(rawHtml); 47 const document = dom.window.document; 48 49 // First the HEAD 50 const head = dom.window.document.getElementsByTagName("head")[0]; 51 52 // add CSS to HEAD 53 head.innerHTML = 54 head.innerHTML + 55 `<meta charset="utf-8">\n` + 56 `<link rel='stylesheet' type='text/css' media='screen' href='../reports-v2.css'>\n`; 57 58 // Assume there's not already a title and that we need to add one. 59 if (dom.window.document.getElementsByTagName("title").length >= 1) { 60 console.log("Already had a <title>… not changing."); 61 } else { 62 const title = document.createElement("title"); 63 const first_h1_text = document.getElementsByTagName("h1")[0].textContent.replace(')Part', ') Part'); 64 title.appendChild(document.createTextNode(first_h1_text)) 65 head.appendChild(title); 66 } 67 68 // calculate the header object 69 const header = dom.window.document.createElement("div"); 70 header.setAttribute("class", "header"); 71 72 // taken from prior TRs, read from the header in 'header.html' 73 header.innerHTML = (await fs.readFile('header.html', 'utf-8')).trim(); 74 75 // Move all elements out of the top level body and into a subelement 76 // The subelement is <div class="body"/> 77 const body = dom.window.document.getElementsByTagName("body")[0]; 78 const bp = body.parentNode; 79 div = dom.window.document.createElement("div"); 80 div.setAttribute("class", "body"); 81 let sawFirstTable = false; 82 for (const e of body.childNodes) { 83 body.removeChild(e); 84 if (div.childNodes.length === 0 && e.tagName === 'P') { 85 // update title element to <h2 class="uaxtitle"/> 86 const newTitle = document.createElement('h2'); 87 newTitle.setAttribute("class", "uaxtitle"); 88 newTitle.appendChild(document.createTextNode(e.textContent)); 89 div.appendChild(newTitle); 90 } else { 91 if (!sawFirstTable && e.tagName === 'TABLE') { 92 // Update first table to simple width=90% 93 // The first table is the document header (Author, etc.) 94 e.setAttribute("class", "simple"); 95 e.setAttribute("width", "90%"); 96 sawFirstTable = true; 97 } 98 div.appendChild(e); 99 } 100 } 101 102 /** 103 * create a <SCRIPT/> object. 104 * Choose ONE of src or code. 105 * @param {Object} obj 106 * @param {string} obj.src source of script as url 107 * @param {string} obj.code code for script as text 108 * @returns 109 */ 110 function getScript({src, code}) { 111 const script = dom.window.document.createElement("script"); 112 if (src) { 113 script.setAttribute("src", src); 114 } 115 if (code) { 116 script.appendChild(dom.window.document.createTextNode(code)); 117 } 118 return script; 119 } 120 121 // body already has no content to it at this point. 122 // Add all the pieces back. 123 body.appendChild(getScript({ src: './js/anchor.min.js' })); 124 body.appendChild(header); 125 body.appendChild(div); 126 127 // now, fix all links from ….md#… to ….html#… 128 for (const e of dom.window.document.getElementsByTagName("a")) { 129 const href = e.getAttribute("href"); 130 let m; 131 if ((m = /^(.*)\.md#(.*)$/.exec(href))) { 132 e.setAttribute("href", `${m[1]}.html#${m[2]}`); 133 } else if ((m = /^(.*)\.md$/.exec(href))) { 134 e.setAttribute("href", `${m[1]}.html`); 135 } 136 } 137 138 // put this last 139 body.appendChild(getScript({ 140 // This invokes anchor.js 141 code: `anchors.add('h1, h2, h3, h4, h5, h6, caption');` 142 })); 143 144 // Now, fixup captions 145 // Look for: <h6>Table: …</h6> followed by <table>…</table> 146 // Move the h6 inside the table, but as <caption/> 147 const h6es = dom.window.document.getElementsByTagName("h6"); 148 const toRemove = []; 149 for (const h6 of h6es) { 150 if (!h6.innerHTML.startsWith("Table: ")) { 151 console.error('Does not start with Table: ' + h6.innerHTML); 152 continue; // no 'Table:' marker. 153 } 154 const next = h6.nextElementSibling; 155 if (next.tagName !== 'TABLE') { 156 console.error('Not a following table for ' + h6.innerHTML); 157 continue; // Next item is not a table. Maybe a PRE or something. 158 } 159 const caption = dom.window.document.createElement("caption"); 160 for (const e of h6.childNodes) { 161 // h6.removeChild(e); 162 caption.appendChild(e.cloneNode(true)); 163 } 164 for (const p of h6.attributes) { 165 caption.setAttribute(p.name, p.value); 166 h6.removeAttribute(p.name); // so that it does not have a conflicting id 167 } 168 next.prepend(caption); 169 toRemove.push(h6); 170 } 171 for (const h6 of toRemove) { 172 h6.remove(); 173 } 174 175 // OK, done munging the DOM, write it out. 176 console.log(`Writing ${outfile}`); 177 178 // TODO: we assume that DOCTYPE is not written. 179 await fs.writeFile(outfile, `<!DOCTYPE html>\n` 180 + dom.serialize()); 181 return outfile; 182} 183 184/** 185 * Convert all files 186 * @returns Promise<String[]> list of output files 187 */ 188async function fixall() { 189 outbox = "./dist"; 190 191 // TODO: move source file copy into JavaScript? 192 // srcbox = '../../../docs/ldml'; 193 194 const fileList = (await fs.readdir(outbox)) 195 .filter((f) => /\.md$/.test(f)) 196 .map((f) => path.join(outbox, f)); 197 return Promise.all(fileList.map(renderit)); 198} 199 200fixall().then( 201 (x) => console.dir(x), 202 (e) => { 203 console.error(e); 204 process.exitCode = 1; 205 } 206); 207