Skip to content

Commit

Permalink
Refactor: Replace special-purpose indexer with generic and configurab…
Browse files Browse the repository at this point in the history
…le node indexer (#70)

Rewrite of the previous special-node-type indexer for nodes of type 'term-occurrence' into a generic indexer to index any AST nodes based on configurable key-selector and filter functions.
  • Loading branch information
about-code committed Jan 26, 2020
1 parent 59fa7b8 commit 4e42b06
Show file tree
Hide file tree
Showing 61 changed files with 1,109 additions and 489 deletions.
31 changes: 24 additions & 7 deletions lib/figures.js → lib/index/figures.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const {root, paragraph, text, heading, brk, link, list, listItem } = require("mdast-builder");

const {getFileLinkUrl} = require("./path/tools");
const {getLinkUrl, getNodeText} = require("./ast/tools");
const {getNodeIndex, getDefinitionIndex, groupByHeading} = require("./indexer");
const {getFileLinkUrl} = require("../path/tools");
const {getLinkUrl, getNodeText} = require("../ast/tools");
const {getIndex, getIndexValues: getValues, groupByHeading} = require("../indexer");

const api = {};

Expand All @@ -12,6 +12,23 @@ const api = {};
* @typedef { import('./indexer').Index } Index
*/

api.indices = [
{
id: "index/figures/nodeType/image"
,keyFn: (indexEntry) => indexEntry.node.type
,filterFn: (indexEntry) => {
const t = indexEntry.node.type;
return t === "image" || t === "imageReference";
}
}
,{
id: "index/figures/nodeType/definition"
,keyFn: (entry) => `${entry.file}#${entry.node.identifier}`
,filterFn: (entry) => entry.node.type === "definition"
}

],

/**
* Returns the markdown abstract syntax tree that is to be written to the file
* configured via 'generateFiles.indexFile' config.
Expand Down Expand Up @@ -94,17 +111,17 @@ function getListOfFiguresItemAst(context, indexEntry) {
* @returns {IndexEntry[]} index entries for node types "image" and "imageReference"
*/
function selectFiguresFromIndex() {
const images = getNodeIndex("image");
const imageRefs = getNodeIndex("imageReference");
const defIndex = getDefinitionIndex();
const images = getValues("index/figures/nodeType/image", "image");
const imageRefs = getValues("index/figures/nodeType/image", "imageReference");
const defIndex = getIndex("index/figures/nodeType/definition");
let figures = [
...images
,...imageRefs.map((indexEntry) => {
// dereference
const refFile = indexEntry.file;
const refNode = indexEntry.node;
const defId = `${refFile}#${refNode.identifier}`;
const defNode = defIndex[defId].node;
const defNode = defIndex[defId][0].node;
indexEntry.node = {
...indexEntry.node
, type: "image"
Expand Down
165 changes: 165 additions & 0 deletions lib/index/terms.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
const {root, paragraph, text, heading, brk, link } = require("mdast-builder");

const Term = require("../model/term");
const {getFileLinkUrl} = require("../path/tools");
const {getLinkUrl, getNodeText} = require("../ast/tools");
const {getIndex, groupByHeading} = require("../indexer");

/**
* @typedef { import("./model/context") } Context
* @typedef { import("./ast/tools").Node } Node
* @typedef {{
* definitions: Term[],
* occurrences: {[path: string]: Node}
* }} IndexOldEntry
* @typedef {{ [term: string]: IndexOldEntry }} IndexOld
*/


/**
* Module internal state. TODO: Try to replace with state specific to an indexer
* plug-in execution.
* @type {Index}
*/
const api = {};
const INDEX_ID = "index/terms/byTerm";
api.indices = [{
id: INDEX_ID
,filterFn: (indexEntry) => indexEntry.node.type === "term-occurrence"
,keyFn: (indexEntry) => indexEntry.node.termDefs[0].term
}];

/**
* Returns the markdown abstract syntax tree that is to be written to the file
* configured via 'generateFiles.indexFile' config.
*
* @param {Context} context
* @returns {Node} mdast tree
*/
api.getAST = function(context) {
const {indexFile} = context.opts.generateFiles;
const indexEntries = getIndex(INDEX_ID);
let title = "";
if (indexFile !== null && typeof indexFile === "object") {
title = indexFile.title;
}

// Create AST from index
let tree = [
heading(1, text(title || "Book Index"))
// Concatenate AST for each index entry
,...Object
.keys(indexEntries)
.sort((key1, key2) => key1.localeCompare(key2, "en"))
.map((key) => getIndexEntryAst(context, indexEntries[key]))
];
return root(tree);
};

/**
*
* @param {Context} context
* @param {IndexEntry} indexEntriesForTerm
* @param {string} indexFilename
* @returns {Node} mdast tree
*/
function getIndexEntryAst(context, indexEntriesForTerm) {
const txtTerm = indexEntriesForTerm[0].node.termDefs[0].term;
return paragraph([
heading(2, text(txtTerm))
,brk
,brk
,...getEntryLinksAst(context, indexEntriesForTerm)
]);
}

/**
*
* @param {Context} context
* @param {IndexEntry} indexEntry
* @param {string} indexFilename
*/
function getEntryLinksAst(context, indexEntriesForTerm) {
const indexFilename = getIndexFilename(context);
const byHeadings = groupByHeading(indexEntriesForTerm);
const links = [
...getGlossaryLinksAst(context, indexEntriesForTerm, indexFilename)
,...getDocumentLinksAst(context, byHeadings, indexFilename)
];
const linksSeparated = [];
for (let i = 0, len = links.length; i < len; i++) {
if (i > 0) {
linksSeparated.push(text(" - "));
} // link separator

linksSeparated.push(links[i]);
}
return linksSeparated;
}

/**
* @param {Context} context
* @param {IndexEntry} indexEntries
* @param {string} fromIndexFilename
* @returns {Node} mdast Node
*/
function getGlossaryLinksAst(context, indexEntries, fromIndexFilename) {
return indexEntries[0].node.termDefs
.sort(Term.compare)
.map((term) => {
const toGlossaryFilename = term.glossary.outPath;
const url = getFileLinkUrl(context, fromIndexFilename, toGlossaryFilename, term.anchor);
return link(url, term.getShortDescription(), text(term.glossary.title));
});
}

/**
* @param {Context} context
* @param {IndexEntry} indexEntry
* @returns {Node} mdast tree
*/
function getDocumentLinksAst(context, byHeadings, fromIndexFilename) {
return byHeadings
.map((indexEntryOccurrences) => {
const indexEntry = indexEntryOccurrences[0]; // [1]
const headingNode = indexEntry.groupHeadingNode;
const toDocumentFilename = indexEntry.file;
const anchor = getLinkUrl(headingNode);
const term = indexEntry.node.termDefs[0];
const ref = getFileLinkUrl(context, fromIndexFilename, toDocumentFilename, anchor);
let linkText;
if (headingNode) {
linkText = getNodeText(headingNode);
} else {
linkText = ref;
}
if (linkText === term.glossary.title) {
// prevent duplicate listing of glossary title (see also getGlossaryLinksAst())
return null;
} else {
return link(ref, null, text(linkText));
}
})
.filter(linkNode => linkNode !== null);

// Implementation Notes:
// [1]: We get the index entries for all occurrences of a particular term
// below the given heading. Since we can only link to the heading but not
// each particular term position we can derive the heading link from the
// first term occurrence, solely.
}

/**
* @deprecated See https://github.com/about-code/glossarify-md/blob/master/CHANGELOG.md#deprecation-notices
* @param {Context} context
*/
function getIndexFilename(context) {
const {indexFile} = context.opts.generateFiles;
if (typeof indexFile !== "string") {
return indexFile.file;
} else {
return indexFile;
}
}

module.exports = api;
Loading

0 comments on commit 4e42b06

Please sign in to comment.