Skip to content

Instantly share code, notes, and snippets.

@shuhei
Created January 25, 2020 23:04
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shuhei/b622af9559d859d386edbfe43f171d72 to your computer and use it in GitHub Desktop.
Save shuhei/b622af9559d859d386edbfe43f171d72 to your computer and use it in GitHub Desktop.
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 `![${alt}](${src}${title})`;
}
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