draaft
Version:
A CLI to pull content from https://pilot.pm content collaboration platform and feed your static site generator with markdown files
240 lines (239 loc) • 10.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const axios_1 = require("axios");
const fs = require("fs-extra");
const matter = require("gray-matter");
const yaml = require("js-yaml");
const _ = require("lodash");
const path = require("path");
const toml = require("@iarna/toml");
const slugify_1 = require("@sindresorhus/slugify");
const signal_1 = require("./signal");
const types_1 = require("./types");
const write = require("./write");
const MARKDOWN_IMAGE_REGEX = /!\[[^\]]*\]\((?<filename>.*?)(?=\"|\))(?<optionalpart>\".*\")?\)/g;
let itemFoldersMap = {};
const matterEngines = {
toml: {
parse: (input) => toml.parse(input),
stringify: (data) => toml.stringify(data),
},
};
class Terraformer {
/**
* Terraform pilot content to SSG content
* @param config - Draaft configuration
*/
constructor(config, destFolder, publicationStateIds) {
this.config = config;
this.destFolder = destFolder;
this.publicationStateIds = publicationStateIds;
}
matterize(content, frontmatter) {
return matter.stringify(content, frontmatter, {
language: this.config.frontmatterFormat,
engines: matterEngines,
delimiters: this.config.frontmatterFormat == types_1.FrontmatterFormat.toml ? "+++" : "---",
});
}
terraformChannel(channel) {
let channelDirPath;
if (this.config.useChannelName) {
let channelSlug = slugify_1.default(channel.name);
channelDirPath = path.join(this.destFolder, channelSlug);
}
else {
channelDirPath = this.destFolder;
}
// Create channel directory
write.ensureDir(channelDirPath);
// Create _index.md file for channel root dir
let frontmatter = _.cloneDeep(channel);
if (this.config.ssg === types_1.SSGType.hugo) {
frontmatter.title = channel.name;
delete frontmatter.name;
delete frontmatter.hierarchy;
delete frontmatter.children;
}
let indexContent = this.matterize(String(frontmatter.description), frontmatter);
write.createContentFile(channelDirPath, "_index.md", indexContent);
this.writeChannelHierarchy(channel.hierarchy, channelDirPath);
}
writeChannelHierarchy(hierarchy, parentDirPath) {
for (let node of hierarchy) {
if (node.type == "folder") {
let folderPath = path.join(parentDirPath, slugify_1.default(node.name));
write.ensureDir(folderPath);
let indexContent = this.matterize(node.name, { title: node.name });
write.createContentFile(folderPath, "_index.md", indexContent);
this.writeChannelHierarchy(node.nodes, folderPath);
}
if (node.type == "item") {
itemFoldersMap[node.id] = parentDirPath;
}
}
}
/**
* With a channel list and all items depending attached to it (on its children) build a directory of .md files
* with a proper directory structure and filename pattern according to user config
*
* @param items : List of items attached to this channel
*/
async terraformItems(items) {
// Currently we write synchronously to have a nice indented terminal output for user, trading speed for UX.
// TODO : Build a report object from async calls to have best of both world.
for (let item of items) {
await this.terraformOneItem(item);
/*
for( let translation of item.translations ){
terraformOneItem(channel, translation, currentFolder, config)
}
*/
}
}
async terraformOneItem(item) {
let itemFolder = itemFoldersMap[item.id];
if (!itemFolder) {
throw `Item ${item.id} has no correspondence in the hierarchy`;
}
let itemDirPath = this.getItemDirPath(itemFolder, item);
let itemFileName = this.getItemFileName(item);
let itemFileContent = await this.getItemFileContent(item, itemDirPath);
write.createContentFile(itemDirPath, itemFileName, itemFileContent);
}
/**
* Build a filepath for content according to Hugo io local config (i18n)
*
* @param document : Draaft document returned by Api
* @param options : Extension configuration object
*/
getItemDirPath(parentFolder, item) {
let itemDirPath = parentFolder;
// first level directory may be 'en' or 'fr' if user decides so
if (this.config.i18nMode === types_1.I18nMode.directory) {
// fr_FR -> fr
let languageCode = item.language
? item.language.split("_")[0]
: this.config.i18nDefaultLanguage;
itemDirPath = path.join(itemDirPath, languageCode);
}
if (this.config.bundlePages) {
itemDirPath = path.join(itemDirPath, this.getItemSlug(item));
}
return itemDirPath;
}
getItemSlug(item) {
return item.title ? `${item.id}-${slugify_1.default(item.title)}` : `${item.id}-notitle`;
}
/**
* Build a filepath for content according to Hugo io local config (i18n)
*
* @param item : Draaft document returned by Api
* @param options : Extension configuration object
*/
getItemFileName(item) {
let itemFileName = this.config.bundlePages ? "index" : this.getItemSlug(item);
if (this.config.i18nMode === types_1.I18nMode.filename && item.language) {
let languageCode = item.language.split("_")[0]; // fr_FR -> fr
itemFileName = itemFileName + "." + languageCode + ".md";
}
else {
itemFileName = itemFileName + ".md";
}
return itemFileName;
}
/**
* Prepare file contents before writing it
*
* @param item : Draaft item returned by Api
*/
async getItemFileContent(item, itemDirPath) {
// Everything from document is in frontmatter (for now, may be updated downwards)
let frontmatter = _.cloneDeep(item);
let markdown = "";
// If we have a content field, use it for markdown source
if (item.content.hasOwnProperty(this.config.contentFieldName)) {
markdown = item.content[this.config.contentFieldName];
markdown = await this.fetchImages(markdown, itemDirPath);
}
// Do we have a local content schema ?
let typeFilePath = `.draaft/type-${frontmatter.item_type}.yml`;
let typefile;
if (fs.existsSync(typeFilePath)) {
typefile = yaml.safeLoad(fs.readFileSync(typeFilePath, "utf8"));
}
if (typefile && typefile.content_schema) {
signal_1.signal.success("Custom type file found, using it");
this.customiseFrontmatter(frontmatter, typefile.content_schema);
}
else {
this.customiseFrontmatter(frontmatter);
}
return this.matterize(markdown, frontmatter);
}
// Take a source content object as map it with local custom content schema
customiseFrontmatter(frontmatter, schema) {
delete frontmatter.channels;
delete frontmatter.targets;
let customTags = [];
frontmatter.tags.forEach((tag) => {
customTags.push(tag.name);
});
frontmatter.tags = customTags;
if (schema) {
// schema will only customise 'frontmatter.content' key, not frontmatter
for (let key of Object.keys(frontmatter.content)) {
// Do not show in frontmatter.content
if (schema[key].fm_show === false) {
delete frontmatter.content[key];
}
// Rename key in frontmatter.content
if (key !== schema[key].fm_key) {
let newKey = schema[key].fm_key;
let oldKey = key;
frontmatter.content[newKey] = frontmatter.content[oldKey];
delete frontmatter.content[oldKey];
}
}
}
else if (frontmatter.content[this.config.contentFieldName]) {
delete frontmatter.content[this.config.contentFieldName];
}
// Translation key
// This is a master trad
if (frontmatter.translations.length) {
frontmatter.translationKey = frontmatter.id;
}
// This is a translation, linked to a master trad
if (frontmatter.master_translation) {
frontmatter.translationKey = frontmatter.master_translation;
}
// Is it published or draft ?
frontmatter.draft = !this.publicationStateIds.includes(frontmatter.workflow_state);
return frontmatter;
}
async fetchImages(markdown, itemDirPath) {
let imagePromises = [];
for (let match of markdown.matchAll(MARKDOWN_IMAGE_REGEX)) {
if (!match.groups || !match.groups.filename) {
continue;
}
let imageUrl = match.groups.filename.trim();
let imageName = imageUrl.split("/").slice(-1)[0];
// If we're bundling images with content, we use a relative ref.
// Else, we'll put it into the statics directory, and use an absolute ref.
let imageRefInMarkdown = this.config.bundlePages ? imageName : "/img/" + imageName;
markdown = markdown.replace(imageUrl, imageRefInMarkdown);
let imagePromise = axios_1.default.get(imageUrl, { responseType: "stream" }).then((response) => {
// If we're bundling images with content, we use the page bundle directory.
// Else, we use the static directory.
let imageDir = this.config.bundlePages ? itemDirPath : write.IMAGE_DIR;
write.createImageFile(imageDir, imageName, response);
});
imagePromises.push(imagePromise);
}
await Promise.all(imagePromises);
return markdown;
}
}
exports.Terraformer = Terraformer;