• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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