skypager-project
Version:
skypager project framework
441 lines (351 loc) • 11.7 kB
JavaScript
import { camelCase, snakeCase, kebabCase } from 'skypager-util/lib/string'
import cloneDeep from 'lodash/cloneDeep'
export default (doc) => {
const ast = doc.ast
if (!doc.ast) {
console.log('Could not find', doc)
}
doc.ast = Object.assign(ast, {
children: ast.children.map(node => decorateNode(doc, node))
})
if (!doc.chain) {
Object.defineProperty(doc, 'chain', {
enumerable: false,
get: function() {
return __non_webpack_require__('lodash').chain(doc)
}
})
}
return Object.assign(doc, {
decorateNode(node) {
return decorateNode(doc, node)
},
createTree() {
const headings = doc.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 = doc.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)
},
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 doc.get('frontmatter.title', doc.variations.humanized)
} else {
return titleNode.textContent()
}
},
introParagraphs() {
const until = doc.selectHeadingNodes()[1] || doc.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})
}
},
withoutNodeIndexes(indexList = []) {
const ast = this.ast
return {
type: 'root',
children: children.filter(node => indexList.indexOf(node.index) === -1),
data: ast.data,
}
},
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()
},
})
}
export function decorateNode(doc, node) {
const original = cloneDeep(node)
return Object.assign(node, {
original() {
return original
},
slug() {
return kebabCase(this.textContent())
},
textContent() {
return require('mdast-util-to-string')(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
})
}
})
}
export function isNode(obj) {
return typeof obj === 'object' && typeof obj.type === 'string' && obj.position
}
export function createBlock(contents, lang='') {
return ["```" + lang, contents, "```"].join("\n")
}