Convert textile files into markdown files
const { promises: fs } = require("fs"); | |
const textile = require("textile-js"); | |
const prettier = require("prettier"); | |
const path = require("path"); | |
async function main() { | |
const postsDir = path.resolve("source", "_posts"); | |
const files = (await fs.readdir(postsDir)).filter(file => | |
file.endsWith(".textile") | |
); | |
for (const file of files) { | |
// Read textile file. | |
const filePath = path.resolve(postsDir, file); | |
const content = await fs.readFile(filePath, "utf8"); | |
console.log(file, content.length); | |
// Extract frontmatter if exists. | |
let frontmatter = ""; | |
let body = content; | |
if (content.startsWith("---")) { | |
const start = content.indexOf("---", 3) + 4; | |
frontmatter = content.slice(0, start); | |
body = content.slice(start); | |
} | |
// Convert textile into markdown. | |
const parsed = textile.jsonml(body, { breaks: false }); | |
await fs.writeFile( | |
path.resolve(postsDir, file.replace(/\.textile$/, ".json")), | |
JSON.stringify(parsed, null, 2) | |
); | |
const markdown = frontmatter + renderMarkdown(parsed); | |
// Format with prettier. | |
const formatted = prettier.format(markdown, { | |
parser: "markdown" | |
}); | |
await fs.writeFile( | |
path.resolve(postsDir, file.replace(/\.textile$/, ".markdown")), | |
formatted | |
); | |
} | |
} | |
const KNOWN_PROPS = { | |
a: ["href", "title"], | |
img: ["src", "alt", "width", "height"], | |
span: ["class"], | |
pre: ["class"] | |
}; | |
const KNOWN_CLASSES = { | |
span: ["caps"], | |
pre: ["prettyprint"] | |
}; | |
function arePropsKnown(tag, props) { | |
const propsCount = Object.keys(props).length; | |
if (propsCount === 0) { | |
return true; | |
} | |
const knownProps = KNOWN_PROPS[tag] || []; | |
if (Object.keys(props).some(p => !knownProps.includes(p))) { | |
return false; | |
} | |
const knownClasses = KNOWN_CLASSES[tag] || []; | |
if (props.class && !knownClasses.includes(props.class)) { | |
return false; | |
} | |
return true; | |
} | |
function renderMarkdown(jsonml) { | |
if (typeof jsonml === "string") { | |
if (jsonml === "\n") { | |
// Remove unnecessary line breaks. | |
return ""; | |
} | |
// textile-js inserts tabs before each list item. | |
if (jsonml === "\n\t") { | |
return ""; | |
} | |
if (jsonml === "\n\t\t") { | |
// Nested list | |
return " "; | |
} | |
if (jsonml === "\n\t\t\t") { | |
// Nested list | |
return " "; | |
} | |
if (jsonml === "\n\t\t\t\t") { | |
// Nested list | |
return " "; | |
} | |
return jsonml; | |
} | |
if (!Array.isArray(jsonml)) { | |
throw new Error("Unexpected element: " + jsonml); | |
} | |
const [tag, ...elements] = jsonml; | |
let props = {}; | |
if (typeof elements[0] === "object" && !Array.isArray(elements[0])) { | |
props = elements[0]; | |
elements.splice(0, 1); | |
if (!arePropsKnown(tag, props)) { | |
console.log(" ", tag, props); | |
} | |
} | |
switch (tag) { | |
case "html": | |
return elements.map(renderMarkdown).join(""); | |
case "p": | |
return elements.map(renderMarkdown).join("") + "\n\n"; | |
case "h2": | |
case "h3": | |
return "## " + elements.map(renderMarkdown).join("") + "\n\n"; | |
case "h4": | |
return "### " + elements.map(renderMarkdown).join("") + "\n\n"; | |
case "pre": | |
return ( | |
"```\n" + | |
makeSureNewLine(elements.map(renderMarkdown).join("")) + | |
"```\n\n" | |
); | |
case "blockquote": | |
return "> " + elements.map(renderMarkdown).join("") + "\n\n"; | |
case "ul": { | |
const nested = elements[0].startsWith("\n\t\t"); | |
return ( | |
(nested ? "\n" : "") + elements.map(renderMarkdown).join("") + "\n" | |
); | |
} | |
case "li": | |
return makeSureNewLine("- " + elements.map(renderMarkdown).join("")); | |
// Convert <dl> to <ul> | |
case "dl": | |
return elements.map(renderMarkdown).join("") + "\n"; | |
case "dt": | |
return "- " + elements.map(renderMarkdown).join("") + ": "; | |
case "dd": | |
return makeSureNewLine(elements.map(renderMarkdown).join("")); | |
// -- Inline elements | |
case "span": | |
return elements.map(renderMarkdown).join(""); | |
case "code": | |
return "`" + elements.map(renderMarkdown).join("") + "`"; | |
case "del": | |
return "~" + elements.map(renderMarkdown).join("") + "~"; | |
case "ins": | |
return "<ins>" + elements.map(renderMarkdown).join("") + "</ins>"; | |
case "strong": | |
return "**" + elements.map(renderMarkdown).join("") + "**"; | |
case "a": { | |
// Check if elements has only text. | |
if (!(elements.length === 1 && typeof elements[0] === "string")) { | |
console.error("Unexpected elements for <a>", elements); | |
} | |
const text = elements.map(renderMarkdown).join(""); | |
const title = props.title ? ` "${props.title}"` : ""; | |
return `[${text}](${props.href}${title})`; | |
} | |
case "img": { | |
const { src, alt, width, height } = props; | |
// Markdown doesn't have syntax for image size. | |
// Encode size at the end of title so that it can be used in post processing. | |
// Ideas from https://github.com/markedjs/marked/issues/339 | |
const title = width && height ? ` "=${width}x${height}"` : ""; | |
return ``; | |
} | |
case "br": | |
return ""; | |
default: | |
throw new Error("Unexpected tag: " + tag); | |
} | |
} | |
function makeSureNewLine(str) { | |
if (str.endsWith("\n")) { | |
return str; | |
} | |
return str + "\n"; | |
} | |
main().then(console.log, console.log); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment