UNPKG

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
"use strict"; 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;