UNPKG

skypager-project

Version:
796 lines (642 loc) 21.8 kB
import yaml from 'js-yaml' import markdown, { utilLoaders as markdownUtils, profiles as markdownProfiles } from '../transformers/markdown' import testRule from 'skypager-util/lib/path-matcher' import { kebabCase, camelCase, snakeCase } from 'skypager-util/lib/string' import omit from 'lodash/omit' import { transpile } from './babel-es6' import lodash from 'lodash' export const rules = [ /\.md$/ ] export const transformPresets = () => { return { markdown: Object.keys(markdownProfiles), } } export const getTransforms = () => { return { markdown: require('../transformers/markdown').default } } export const getUtils = () => { const loaders = markdownUtils() return Object.keys(loaders).reduce((memo, key) => ( Object.assign(memo, { get [key]() { return loaders[key]() } }) ), {}) } export const compile = (contents, options = {}) => { const profile = options.profile || 'standard' const html = 'html' try { const ast = toAST(contents, { profile }) return markdown({profile:'html'}).stringify(ast) } catch(error) { const stack= ["```", error.stack, "```"].join("\n") return markdown({profile:'html'}) .stringify( toAST(`# Error\n\n${error.message}\n\n${stack}\n`) ) } } export const toAST = (contents, options) => { const profile = options.profile || 'standard' try { return markdown(profile, options).parse(contents) } catch(error) { return markdown(profile, options).parse(`# Error\n\n${error.message}\n\n${createBlock(error.stack)}\n## Contents\n${createBlock(contents)}`) } } export const testDocument = (doc) => ( typeof rules.find(rule => testRule(rule, doc.fileBaseName)) !== 'undefined' ) export const decorate = (doc) => { doc.lazy('exportables', () => doc.createExportable()) doc.lazy('exports', () => doc.createExportable()) Object.assign(doc, { stringify() { return this.markdown('stringify', {}).stringify(doc.ast) }, toCode(options = {}) { const codeBlocks = doc.compileCodeBlocks({keyBy:'accessor'}) return codeBlocks.mapValues((block, id) => (` // ${ block.docPath } at ${ block.index } ${block.value} `.trim())).values().join("\n\n") }, toModule(options = {}) { return doc.project.createModule(doc.toCode(), options) }, toEntity(selectorMap = {}) { const obj = doc.exportables const frontmatter = obj.frontmatter const instance = Object.assign({}, obj, frontmatter) if (lodash.isEmpty(selectorMap)) { Object.assign(instance, doc.se) } }, transformWith(transformerFn, options = {}) { const doc = this transformerFn.call(doc, doc, options) return doc }, createExportable(options = {}) { const html = doc.markdown('html', options) const source = typeof options.transform === 'function' ? doc.transformWith(options.transform, options) || doc : doc const contents = source.contents || source.result('file.contents', () => doc.readFile().get('file.contents', '')) const exportable = Object.assign({}, doc.frontmatter, { id: source.id, get contents() { return contents }, get frontmatter() { return doc.frontmatter }, get ast() { return source.ast }, get file() { return omit(doc.file, 'contents') }, get html() { return doc.remarkHTML({ ast: source.renderAST || source.ast }) } }) return exportable }, compileCodeBlocks(options = {}, context = {}) { const babelRc = doc.result('project.babelRc', {presets:['skypager'],plugins:['transform-decorators-legacy']}) return doc .selectChain('code-blocks', options) .mapValues((nodes) => { return nodes.map(node => { let result try { result = transpile(node.value, babelRc) } catch(error) { result = {error, code: '', metadata: {}} } Object.assign(node, { run() { if (result.error) { return Promise.resolve(Object.assign(node, {error})) } return doc.asyncRunner(result.code)() .then((result) => { return Object.assign(node, { get result() { return typeof result.value === 'function' ? result.value() : result }, inspectResult() { return require('util').inspect(node.result) } }) }) .catch((error) => { return Object.assign(node, {error}) }) }, get compiled() { return result.code }, get metadata() { return result.metadata } }) return node }) }) }, updateFrontmatter(data = {}, save = true) { const ast = doc.ast let frontmatter = doc.frontmatter if (!this.hasFrontmatter()) { this.ast.children.unshift({}) } frontmatter = Object.assign(frontmatter, {updatedAt: (Math.floor(new Date() / 1000))}, data) const { children } = doc.markdown().parse(`---\n${yaml.safeDump(frontmatter)}\n---\n`) this.ast.children[0] = children[0] if (save) { const contents = doc.contents = doc.file.contents = doc.stringify() doc.project.writeFileSync(doc.path, contents) } return this }, hasFrontmatter() { const zero = this.firstNode() return zero && zero.type === 'yaml' }, remarkAST(options = {}) { const profile = options.profile || 'standard' const contents = options.contents || this.contents || this.getFileContents() try { const result = doc.markdown(profile, options).parse(contents) return Object.assign(result, { children: result.children.map(node => doc.decorateNode(node)) }) } catch(error) { return doc.markdown(profile, options).parse(`# Error\n\n${error.message}\n\n${createBlock(error.stack)}\n## Contents\n${createBlock(contents)}`) } }, remarkHTML(options = {}) { const profile = options.profile || 'standard' const parser = options.parser || doc.markdown(profile, options) const contents = !options.ast && (options.contents || this.contents || this.getFileContents()) const ast = options.ast || parser.parse(contents) const html = 'html' try { return doc.markdown(html, options).stringify(ast) } catch(error) { const stack= ["```", error.stack, "```"].join("\n") return doc .markdown(html, {}) .stringify( doc.markdown(profile, {}).parse(`# Error\n\n${error.message}\n\n${stack}\n`) ) } }, markdown(profile = 'parser', options) { return markdown({ profile, ...options }) }, createTree() { const headings = this.createHeadingsMap('headingPath') const tree = {} Object.keys(headings).forEach(headingPath => { const treePath = headingPath.split('/').map(v => camelCase(kebabCase(v))) lodash.set(tree, treePath, headings[headingPath]) }) return tree }, createHeadingsMap(keyBy = 'accessor', stayChained = false) { const obj = this.chain .invoke('selectHeadingNodes') .map(heading => { heading.accessorMethod = camelCase(snakeCase(heading.textContent())) heading.headingPath = heading.getHeadingPath() heading.accessors = heading.headingPath.split('/').map(i => camelCase(snakeCase(i))) heading.accessor = camelCase(snakeCase(heading.headingPath.replace(/\//g,'_'))) return heading }) .keyBy(keyBy) return stayChained ? obj : obj.value() }, stringifyNode(node) { return require('mdast-util-to-string')(node) }, getSourceForNode(node) { return require('unist-util-source')(node) }, decorateNode(node) { const original = lodash.cloneDeep(node) return Object.assign(node, { original() { return original }, slug() { return lodash.kebabCase(this.textContent()) }, textContent() { return doc.stringifyNode(node) || '' }, getHeadingPath() { if (node.type !== 'heading') { const prev = node.previousHeading() return prev && prev.getHeadingPath() } const textContent = kebabCase(node.textContent().toLowerCase()) const prevSuperior = node.getPreviousSuperior() const previousPath = typeof prevSuperior === 'undefined' ? `` : (prevSuperior.depth >= 2 && prevSuperior.getHeadingPath()) || '' return [previousPath, textContent].filter(i => i && i.length > 0).join('/') }, headingRangeSync() { const alpha = node const bravo = node.getFinalHeading() || doc.finalNode() return doc.getNodesBetweenPositions(alpha, bravo || 50000).value() }, headingRange(test = node.textContent()) { const ast = doc.ast return new Promise((resolve,reject) => { try { require('mdast-util-heading-range')(ast, test, (startNode, nodes, endNode) => { resolve({startNode, nodes, endNode}) }) } catch(error) { reject(error) } }) }, getFinalHeading() { return doc.findAllNodesAfter(node, (n) => n.type === 'heading' && n.depth <= node.depth )[0] }, getPreviousSuperior() { return node && node.getNodesBefore && node.getNodesBefore(n => n.type === 'heading' && (n.depth < node.depth))[0] }, previousHeading() { return node.getNodesBefore(n => n.type === 'heading').shift() }, nextHeading() { return node.type === 'heading' ? node.getNextNode((n) => n.type === 'heading' && n.depth >= node.depth) : node.getNextNode(n => n.type === 'heading') }, getNextNode(...args) { return doc.findNodeAfter(node, ...args) }, getNodesAfter(...args) { return doc.findAllNodesAfter(node, ...args) }, getNodesBefore(...args) { return doc.findAllNodesBefore(node, ...args) }, selectChildren(selector, testFn) { if(node.type === 'heading') { return node.selectHeadingChildren(selector, testFn) } const results = require('unist-util-select')(node)(selector) return typeof testFn === 'function' ? results.filter(r => testFn.call(doc, r)) : results }, virtualAST() { return { type: 'root', children: node.headingRangeSync().map(n => n.original()).map((n,i) => { n.index = i return n }) } }, selectHeadingChildrenSync(selector = '*', testFn) { let results = require('unist-util-select')(node.virtualAST())(selector) results = typeof testFn === 'function' ? results.filter(r => testFn.call(doc, r)) : results return results }, selectHeadingChildren(selector = '*', testFn) { return node.headingRange() .then(({nodes}) => { const base = { type: 'root', children: nodes } return require('unist-util-select')(base)(selector) }) .then((results) => { return typeof testFn === 'function' ? results.filter(r => testFn.call(doc, r)) : results }) } }) }, headingRange(test) { const ast = doc.ast return new Promise((resolve,reject) => { try { require('mdast-util-heading-range')(ast, test, (startNode, nodes, endNode) => { resolve({startNode, nodes, endNode}) }) } catch(error) { reject(error) } }) }, documentTitle() { const titleNode = doc.titleNode() if (!titleNode) { return this.get('frontmatter.title', doc.variations.humanized) } else { return titleNode.textContent() } }, yamlBlocks() { return this.selectNodes('code[lang="yaml"], yaml') }, introParagraphs() { const until = this.selectHeadingNodes()[1] || this.finalNode() if (!until) { return doc.selectNodes('paragraph') } else { const children = until.getNodesBefore((node) => ( ['paragraph'].indexOf(node.type) >= 0 )).reverse() return require('mdast-util-to-string')({type:'root', children}) } }, wrapSections() { const asts = this.sectionHeadingNodes() .map(node => this.createAstFromSection(node, { offset: false, type: 'div', data: { hName: 'section', hProperties: { 'data-section-path': node.getHeadingPath(), 'data-node-index': node.index, }, } })) return asts }, createDocumentFromSection(node, options = {}) { const { startNode = node, endNode, nodes = [] } = node.headingRange() const ast = doc.createAstFromSection(startNode, nodes) return { id: `${doc.id}/~/${node.headingPath || node.getHeadingPath()}`, ast, contents: require('unist-util-source')(ast), } }, createAstFromSection(node, children, options = {}) { const startDepth = node.depth const offset = options.offset !== false ? 1 - startDepth : 0 children = children .map(node => node.original ? node.original() : node) .map(child => child.type === 'heading' ? (Object.assign(child, {depth: child.depth + offset})) : child ) const newAST = { type: options.type || 'root', children, data: options.data || {}, } return newAST }, withoutNodeIndexes(indexList = []) { const ast = this.ast return { type: 'root', children: children.filter(node => indexList.indexOf(node.index) === -1), data: ast.data, } }, replaceSection(sectionNode, newNode) { return this.extractSectionNodes(sectionNode) .then((nodes) => this.withoutNodeIndexes(nodes.map(n => n.index))) .then((newAst) => { return newAst }) }, createContentNode(tagName, attributes = {}, value) { const node = { type: 'div', data: { hName: tagName || 'div', hProperties: attributes, } } if (typeof value === 'string') { node.value = value } return node }, createAST(options = {}) { const newAST = { type: options.type || 'root', children: options.children || [], data: options.data || {}, } return newAST }, extractSectionNodes(node) { return node.headingRange().then(({nodes}) => nodes) }, inspectOutline() { const headings = this.selectHeadingNodes() return headings.map(headingNode => { const content = `- ${headingNode.textContent()}` return lodash.padStart(content, content.length + (headingNode.depth - 1) * 2) }).join("\n") }, headingIndexes(depth) { return this.chain .invoke('selectHeadingNodes', depth && `[depth=${depth}]`) .sortBy(node => node.position ? node.position.start.line : node.index) .map(node => node.index) .value() }, references() { return Object.assign({}, this.referenceImages(), this.referenceLinks()) }, referenceImages() { return this.chain .invoke('selectNodes', 'imageReference') .keyBy('identifier') .mapValues((refLink, identifier) => { const definitionNodes = doc.definitionNodes() return Object.assign(refLink, { definitionNode() { return definitionNodes[identifier] }, get definition() { return lodash.pick(definitionNodes[identifier], 'title', 'url', 'identifier') } }) }) .value() }, imageNodes() { return this.selectNodes('image') }, referenceLinks() { return this.chain .invoke('selectNodes', 'linkReference') .keyBy('identifier') .mapValues((refLink, identifier) => { const definitionNodes = doc.definitionNodes() return Object.assign(refLink, { definitionNode() { return definitionNodes[identifier] }, get definition() { return lodash.pick(definitionNodes[identifier], 'title', 'url', 'identifier') } }) }) .value() }, definitionNodes() { return this.chain .invoke('selectNodes', 'definition') .keyBy('identifier') .value() }, selectHeadingsByDepth(depth = 1) { return this.selectHeadingNodes(`[depth=${depth}]`) }, selectOneNode(selector) { return require('unist-util-select')(this.ast)(selector)[0] }, selectNodes(selector = '*', testFn) { const results = require('unist-util-select')(this.ast)(selector) return typeof testFn === 'function' ? results.filter(r => testFn.call(doc, r)) : results }, selectHeadingNodes(enhance = '') { return doc.selectNodes(`heading${enhance}`.trim()) }, selectCodeBlocks(lang = 'javascript', enhance='') { return doc.selectNodes(`code[lang=${lang}]${enhance}`.trim()) }, findNodeAfter(node, ...args) { return require('unist-util-find-after')(this.ast, node, ...args) }, findAllNodesAfter(node, ...args) { return require('unist-util-find-all-after')(this.ast, node, ...args) }, findAllNodesBefore(node, ...args) { return require('unist-util-find-all-before')(this.ast, node, ...args) }, nodesIndexedByPosition() { return doc.chain .get('ast.children') .keyBy(node => parseInt(node.position.start.line)) .value() }, introNodes() { const f = this.selectNodes('heading') const m = f.find(n => n.depth > 1) const nodes = m ? m.getNodesBefore().reverse() : this.ast.children return nodes.filter(n => n.type !== 'yaml') }, finalHeading() { return this.selectNodes('heading:last-of-type') }, titleNode() { return this.selectOneNode('heading[depth=1]') || this.selectOneNode('heading:first-of-type') }, firstSectionHeadingNode() { return this.selectOneNode('heading[depth=2]') }, sectionHeadingNodes() { return this.selectNodes('heading[depth=2]') }, articleHeadingNodes() { return this.selectNodes('heading[depth=3]') }, subarticleHeadingNodes() { return this.selectNodes('heading').filter(n => n.depth >= 4) }, getNodesBetweenPositions(alpha, bravo) { const start = isNode(alpha) ? alpha.position.start.line : parseInt(alpha) const end = isNode(bravo) ? bravo.position.start.line : parseInt(bravo) return doc.chain.get('ast.children').filter(node => { const { position: { start: { line } }} = node return line > start && line < end }) }, firstNode() { return this.chain.get('ast.children', []).first().value() }, finalNode() { return this.chain.get('ast.children', []).last().value() }, getAST() { const _ast = doc.remarkAST() return Object.assign(_ast, { children: _ast.children.map((n,i) => { const d = doc.decorateNode(n) d.index = i return d }), }) }, getResults() { const codeBlocks = doc.compileCodeBlocks().values().flatten().invokeMap('run').thru(r => Promise.all(r)).value() return codeBlocks .then((results) => { doc.hide('codeBlockResults', results) }) .then(() => { return this }) } }) doc.lazy('ast', () => doc.getAST()) doc.lazy('renderAST', () => { return doc.remarkAST() }) doc.hide('parseFrontmatter', (val) => { if (!val) { const firstNode = doc.get('ast.children[0]', {}) val = firstNode.type === 'yaml' ? firstNode.value : undefined } return val && val.length > 0 ? yaml.safeLoad(val) : {} }) doc.lazy('frontmatter', () => { const firstNode = doc.get('ast.children[0]', {}) return firstNode.type === 'yaml' ? yaml.load(firstNode.value) : {} }) doc.lazy('headingsMap', () => doc.createHeadingsMap()) return doc } export default decorate function isNode(obj) { return typeof obj === 'object' && typeof obj.type === 'string' && obj.position } function createBlock(contents, lang='') { return ["```" + lang, contents, "```"].join("\n") }