Created
January 25, 2020 23:04
-
-
Save shuhei/b622af9559d859d386edbfe43f171d72 to your computer and use it in GitHub Desktop.
Convert textile files into markdown files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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