Skip to content

Commit

Permalink
63 allow to configure heading depth for less cluttered index file (#71)
Browse files Browse the repository at this point in the history
feat: Allow configuring heading depth for less cluttered index file (#63)

You can now use option `indexing.groupByHeadingDepth` to adjust the granularity
of section links in an index file. So for example for `groupByHeadingDepth: 1`
the term index will only print the chapters of occurrence. Sections of
occurrence are listed in numbered subscript links.

docs: API doc
test: New baseline.
  • Loading branch information
about-code committed Feb 9, 2020
1 parent 4e42b06 commit 04ed7f8
Show file tree
Hide file tree
Showing 17 changed files with 274 additions and 84 deletions.
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,11 +262,30 @@ When true any occurrence of a term will be linked no matter how it was spelled.

Paths or Glob-Patterns for files to include.

### `--indexing.groupByHeadingDepth`

- **Range:** `number` in [1-6]
- **Since:** v3.4.0

This option affects outputs generated with `generateFiles`. By default when
indexing terms and markdown elements they are being grouped by the heading of
the section they've been found in. In larger books with a lot of sections and
subsections this can lead to *Index* files or *Tables of X* to be generated with
lots of low-level sections and much detail. Yet sometimes it may be preferable
to only list the book chapter or high-level sections which some element has been
found in. This option allows to set the depth by which indexed elements shall be
grouped where `1` refers to chapters (`#` headings). Note that grouping by
high-level sections doesn't mean that only links to the high-level sections are
generated. Where it makes sense links to low-level sections of occurrence are
just being shortened.

### `--keepRawFiles` | `--r`

- **Range:** `string[]`

Paths or Glob-Patterns for (markdown) files to copy to `outDir` but ignore in glossarification and linking. Non-markdown files will always be kept as is so no need to add those.
Paths or Glob-Patterns for (markdown) files to copy to `outDir` but ignore in
glossarification and linking. Non-markdown files will always be kept as is so no
need to add those.

### `--linking` | `--l`

Expand Down
4 changes: 2 additions & 2 deletions conf.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@
"type": "object",
"properties": {
"groupByHeadingDepth": {
"description": "Level of detail by which to group occurrences of terms or syntactic elements in generated files (Range [min; max] = [0; 9]). For example, use 0 to not group at all; 1 to group things at the level of document titles, etc. Configures the indexer. The option affects any files generated from the internal AST node index.",
"description": "Level of detail by which to group occurrences of terms or syntactic elements in generated files (Range [min, max]: [0, 6]). For example, use 0 to not group at all; 1 to group things at the level of document titles, etc. Configures the indexer. The option affects any files generated from the internal AST node index.",
"type": "number",
"minimum": 0,
"maximum": 9,
"maximum": 6,
"default": 0
}
}
Expand Down
19 changes: 12 additions & 7 deletions lib/index/figures.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const {root, paragraph, text, heading, brk, link, list, listItem } = require("md

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

const api = {};

Expand All @@ -26,8 +26,7 @@ api.indices = [
,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
Expand Down Expand Up @@ -59,20 +58,26 @@ api.getAST = function(context) {
*/
function getFiguresBySectionAst(context, figures) {
return paragraph(
groupByHeading(figures).map((figures) => {
group(figures, byGroupHeading).map((figures) => {
const groupHeadingNode = figures[0].groupHeadingNode;
return paragraph([
// add +1 to depth of headings referred to in order to keep
// the title of the generated file the only depth-1 heading
brk
,heading(groupHeadingNode.depth + 1, text(getNodeText(groupHeadingNode)))
,heading(groupHeadingNode.depth + 1, // [1]
text(getNodeText(groupHeadingNode))
)
,brk
,brk
,getListOfFiguresAst(context, figures)
,brk
]);
})
);
/**
* Implementation Notes:
*
* [1] add +1 to depth of headings referred to in order to keep
* the title of the generated file the only depth-1 heading
*/
}

/**
Expand Down
55 changes: 44 additions & 11 deletions lib/index/terms.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const {root, paragraph, text, heading, brk, link } = require("mdast-builder");
const {root, paragraph, text, heading, brk, link, html} = require("mdast-builder");

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

/**
* @typedef { import("./model/context") } Context
Expand All @@ -22,9 +22,8 @@ const {getIndex, groupByHeading} = require("../indexer");
* @type {Index}
*/
const api = {};
const INDEX_ID = "index/terms/byTerm";
api.indices = [{
id: INDEX_ID
id: "index/terms/byTerm"
,filterFn: (indexEntry) => indexEntry.node.type === "term-occurrence"
,keyFn: (indexEntry) => indexEntry.node.termDefs[0].term
}];
Expand All @@ -38,7 +37,7 @@ api.indices = [{
*/
api.getAST = function(context) {
const {indexFile} = context.opts.generateFiles;
const indexEntries = getIndex(INDEX_ID);
const indexEntries = getIndex("index/terms/byTerm");
let title = "";
if (indexFile !== null && typeof indexFile === "object") {
title = indexFile.title;
Expand Down Expand Up @@ -81,17 +80,16 @@ function getIndexEntryAst(context, indexEntriesForTerm) {
*/
function getEntryLinksAst(context, indexEntriesForTerm) {
const indexFilename = getIndexFilename(context);
const byHeadings = groupByHeading(indexEntriesForTerm);
const byHeadings = group(indexEntriesForTerm, byGroupHeading);
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(text(" \u25cb "));
}
linksSeparated.push(links[i]);
}
return linksSeparated;
Expand Down Expand Up @@ -137,18 +135,53 @@ function getDocumentLinksAst(context, byHeadings, fromIndexFilename) {
// prevent duplicate listing of glossary title (see also getGlossaryLinksAst())
return null;
} else {
return link(ref, null, text(linkText));
const occurrencesAst = getLinksToOccurrenceAst(context, fromIndexFilename, indexEntryOccurrences);
if (occurrencesAst.length === 0) {
return link(ref, null, text(linkText));
} else {
return paragraph([
link(ref, null, text(linkText))
,html("<sub> ")
,...occurrencesAst
,html("</sub>")
]);
}
}
})
.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.
}

function getLinksToOccurrenceAst(context, fromIndexFilename, indexEntries) {
let {groupByHeadingDepth} = context.opts.indexing;
let i = 1;
return group(indexEntries, byHeading)
.map((entriesByHeading) => {
const { headingNode, file } = entriesByHeading[0];
if (headingNode && headingNode.depth > groupByHeadingDepth) {
const anchor = getLinkUrl(headingNode);
const ref = getFileLinkUrl(context, fromIndexFilename, file, anchor);
return link(ref, getNodeText(headingNode), text(`${i++} `));
}
})
.filter(html => html !== undefined);
}

function byHeading(indexEntry) {
const groupHeadingNode = indexEntry.headingNode;
const anchor = getLinkUrl(groupHeadingNode) || "";
let pos = "0";
if (groupHeadingNode) {
pos = groupHeadingNode.position.start.line;
}
// return key
return `${indexEntry.file}#${pos}:${anchor}`;
}

/**
* @deprecated See https://github.com/about-code/glossarify-md/blob/master/CHANGELOG.md#deprecation-notices
* @param {Context} context
Expand Down
91 changes: 66 additions & 25 deletions lib/indexer.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ const _index = {};
let _indexEntryId = 0;

/**
* Builds an index of nodes by type and an index of definitions by id.
* The index scope spans accross any visited files.
* Builds up indices from the `indices` configuration provided with `opts`.
*
* @param {Options} opts
* @returns {(tree: Node, vFile) => Node} mdast transformer
*/
Expand All @@ -45,25 +45,29 @@ api.indexer = function(opts) {
};

/**
* Returns the visitor that visits and indexes all the nodes of an AST according
* to the `indices` indexer option.
*
* @param {Options} opts indexer options
* @param {string} file currently visited document filename
* @returns {(node: Node) => void}
*/
function getNodeVisitor(opts, file) {
const {context} = opts;
const indices = opts.indices || [];
const locate = getHeadingNodeLocator(context);
const setHeadingNodes = getHeadingNodesSetter(context);
return function visitor(node) {
_indexEntryId++;
const indexEntry = {
id: _indexEntryId
,node: node
,file: file
,headingNode: null
,groupHeadingNode: null
,file: file
};
locate(indexEntry);
setHeadingNodes(indexEntry);

// Indexing...
for (let i = 0, len = indices.length; i < len; i++) {
const idx = indices[i];
const id = idx.id;
Expand All @@ -81,17 +85,31 @@ function getNodeVisitor(opts, file) {
};
}

function getHeadingNodeLocator(context) {
/**
* Returns a function which takes an indexEntry and sets its `headingNode`
* and `groupHeadingNode` nodes based on the last visited heading nodes.
* ### Group Heading Node vs. Heading Node
*
* The `groupHeadingNode` depends on the configuration option
* `indexing.groupByHeadingDepth` and is the last visited heading node with a
* depth less or equal to `groupByHeadingDepth`. The group headings are used
* to decide the level of detail for rendering where some indexed item can be
* found in a book. For example users tell with a group heading depth of 1
* that they are interested only in the chapters where some indexed markdown
* element can be found but not necessarily the exact section. Nevertheless
* a reference to the exact section will also be stored with an index entry in
* `headingNode`.
*
* @param {Context} context
*/
function getHeadingNodesSetter(context) {
let {groupByHeadingDepth} = context.opts.indexing;
let lastVisitedGroupHeading = null;
let lastVisitedHeading = null;
if (! groupByHeadingDepth ) {
groupByHeadingDepth = 9;
}

return function locate(indexEntry) {
return function setter(indexEntry) {
const node = indexEntry.node;
if (node.type === "heading" && node.depth <= 9) {
if (node.type === "heading" && node.depth <= 6) {

// Remember section of current node. Remember section at a depth
// configured via option 'indexing.groupByHeadingDepth'.
Expand All @@ -114,12 +132,20 @@ function getHeadingNodeLocator(context) {
};
}

/**
* @param {string} indexId Id of an index as has been passed with `opts` to `indexer(opts)`
*/
api.getIndex = function(indexId) {
if (Object.prototype.hasOwnProperty.call(_index, indexId)) {
return _index[indexId];
}
};

/**
* @param {string} indexId Id of an index as has been passed with `opts` to `indexer(opts)`
* @param {string} key particular index key
* @returns {IndexEntry[]} index entries mapped onto key or empty array.
*/
api.getIndexValues = function(indexId, key) {
const index = api.getIndex(indexId);
if (index) {
Expand All @@ -130,25 +156,25 @@ api.getIndexValues = function(indexId, key) {
};

/**
* Returns an array of arrays where each `arr[i]` represents a list of index
* entries and each `arr[i][j]` an `IndexEntry` for some AST node belonging to
* a document heading used to group the nodes. So all `item[i][j].groupHeadingNode`
* refer to the same heading AST node.
* Returns an array where each `arr[i]` represents another array of index
* entries that share the same group heading. Each `arr[i][j]` is an `IndexEntry`
* for some AST node where `item[i][j].groupHeadingNode` is the same heading AST
* node for every j at a given i.
*
* item[i][j].headingNode and each item[i][j] an occurre
* @returns {Array<Array<IndexEntry>>}
* @param {IndexEntry[]} indexEntries Index entries to group by their `groupHeadingNode`
* @param {(IndexEntry) => string} [groupKeyFn] Function returning the key value to group by. If missing groups by `groupHeadingNode`
* @returns {Array<IndexEntry[]>}
*/
api.groupByHeading = function(indexEntries) {
api.group = function(indexEntries, groupKeyFn) {
const groups = {};
for (let i = 0, len = indexEntries.length; i < len; i++) {
const indexEntry = indexEntries[i];
const groupHeadingNode = indexEntry.groupHeadingNode;
const anchor = getLinkUrl(groupHeadingNode) || "";
let pos = "0";
if (groupHeadingNode) {
pos = groupHeadingNode.position.start.line;
let key = null;
if (! groupKeyFn) {
key = api.byGroupHeading(indexEntry);
} else {
key = groupKeyFn.call(null, indexEntry);
}
const key = `${indexEntry.file}#${pos}:${anchor}`;
if (! groups[key]) {
groups[key] = [];
}
Expand All @@ -162,4 +188,19 @@ api.groupByHeading = function(indexEntries) {
});
};

/**
* Group key function to use with `group(indexEntries, groupKeyFn)`
* in order to group index entries by their `groupHeadingNode`.
*/
api.byGroupHeading = function(indexEntry) {
const groupHeadingNode = indexEntry.groupHeadingNode;
const anchor = getLinkUrl(groupHeadingNode) || "";
let pos = "0";
if (groupHeadingNode) {
pos = groupHeadingNode.position.start.line;
}
// return key
return `${indexEntry.file}#${pos}:${anchor}`;
};

module.exports = api;
4 changes: 4 additions & 0 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,16 @@ function prepare(context) {
if (! opts.indexing ) {
opts.indexing = {};
}
if (! opts.indexing.groupByHeadingDepth) {
opts.indexing.groupByHeadingDepth = 6;
}
if (! opts.generateFiles ) {
opts.generateFiles = {};
}
if (! opts.glossaries ) {
opts.glossaries = {};
}

return Promise.resolve(context);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## [Term](#term)

[Glossary][1] - [Term][2]
[Glossary][1] [Term][2]

[1]: ./sub/glossary.md#term "GIVEN a term 'Term' AND option 'indexFile' is './index.md' AND the glossary file is in './sub/glossary.md' AND config option 'linking' is 'relative'
THEN the term MUST be linked with a path './sub/glossary.md#term'."
Expand Down

0 comments on commit 04ed7f8

Please sign in to comment.