Skip to content

Commit

Permalink
fix: v3.0.0 linkified headings don't play well with vuepress. Closes #48
Browse files Browse the repository at this point in the history
.

Term indexing has been completely rewritten and glossarify-md now uses
AST-based indexing and index file generation. With the new dedicated
'indexer' unifiedjs processor plug-in we no longer need to do indexing
and linking in the same processor plug-in.

This caused the bug: in v3.0.0 we did linking and indexing in the same
processor plug-in which required us to run 'remark_link_headings' prior
to running our 'linker'. Only this way we had the heading link information
available which we need for creating links from the index file to those
sections.

However, this brought us in a bad situation where we had to apply
'remark_link_headings' prior to the 'linker' and 'remark_ref_links'
after the 'linker'. In this situation markdown headings were alway
converted to reference link syntax '[My Heading][1]` which vuepress or
'vuepress-bar' didn't recognize and kept in the eventual HTML results
(GitHb was fine, though).

The new implementation with the separate processor plug-in is now
much cleaner and allows us run the 'linker' step first, then convert
any generated glossary links via 'remark_ref_links' and after that
linkify headings into '[My Heading](#my-heading)' syntax and then
apply the 'indexer'.
  • Loading branch information
about-code committed Dec 19, 2019
1 parent 0c0501a commit e9485f2
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 157 deletions.
22 changes: 20 additions & 2 deletions conf.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,30 @@
"type": "object",
"properties": {
"indexFile": {
"description": "Path relative to 'outDir' where to write an index file with glossary terms and links to there occurrences in text.",
"type": "string",
"description": "Generate a file with a list of glossary terms and where they have been used.",
"oneOf": [{
"type": "string"
}, {
"type": "object",
"$ref": "#/$defs/IndexFile"
}],
"default": ""
}
}
},
"IndexFile": {
"type": "object",
"properties": {
"file": {
"description": "Path relative to 'outDir' where to create the index markdown file.",
"type": "string"
},
"title": {
"description": "The page title for the index file. If missing the application uses a default value.",
"type": "string"
}
}
},
"Glossary": {
"type": "object",
"properties": {
Expand Down
12 changes: 12 additions & 0 deletions lib/ast-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ api.getNodeText = function getNodeText(node) {
}
}

api.getLinkUrl = function getLinkUrl(node) {
if (! node) {
return;
} else if (node.type === "link") {
return node.url;
} else if (node.children && node.children.length > 0) {
return getLinkUrl(node.children[0]);
} else {
return;
}
}

/**
* No-op compiler to satisfy unifiedjs
* @private
Expand Down
38 changes: 0 additions & 38 deletions lib/index.js

This file was deleted.

183 changes: 183 additions & 0 deletions lib/indexer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
const uVisit = require('unist-util-visit');
const {root, paragraph, text, heading, brk, link } = require('mdast-builder');
const url = require('url');
const path = require('path');

const {relativeFromTo, toForwardSlash} = require('./pathplus');
const {getLinkUrl: getMarkdownLinkUrl, getNodeText} = require('./ast-tools.js');

/**
* Index built when using unified indexer() plug-in.
*
* {
* "term": {
* definitions: [Term, Term],
* occurrences: {
* "./document1#foo": { headingNode: Node }
* "./document2#bar": { headingNode: Node }
* }
* }
* }
*/
const index = {}


/**
* Unified plug-in to scan for links to glossary terms and remember their
* section of use for index file generation.
*/
function indexer(context) {
const indexFilename = getIndexFilename(context);
if (! indexFilename) {
return () => (tree, vFile) => {};
} else {
return () => (tree, vFile) => {
const currentDocFilename = `${vFile.dirname}/${vFile.basename}`;
uVisit(tree, 'term-occurrence', getNodeVisitor(context, indexFilename, currentDocFilename));
};
}
}

function getNodeVisitor(context, fromIndexFile, toDocumentFile) {
return function visitor(node) {
const {termDefs, headingNode} = node;
let headingAnchor;
if (headingNode) {
headingAnchor = getMarkdownLinkUrl(headingNode);
} else {
headingAnchor = "";
}

// Get URL from index file to the section (heading) in which the term was found
const docRef = getFileLinkUrl(context, fromIndexFile, toDocumentFile, headingAnchor)
const term = termDefs[0].term;
if (! index[term]) {
index[term] = {
definitions: termDefs,
occurrences: {
[docRef]: { headingNode }
}
}
}
};
}

/**
* Returns the filename relative to 'outDir' as given by glossarify-md config
*
* @param {} context
*/
function getIndexFilename(context) {
const { indexFile } = context.opts.generateFiles;
if (indexFile && typeof indexFile === "object") {
return indexFile.file;
} else {
return indexFile;
}
}

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

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

function getIndexEntryAst(context, indexEntry, indexFilename) {
return heading(4, [
text(indexEntry.definitions[0].term),
brk,
paragraph(
getEntryLinksAst(context, indexEntry, indexFilename)
)
]);
}

function getEntryLinksAst(context, indexEntry, indexFilename) {
const links = [
...getGlossaryLinksAst(context, indexEntry, indexFilename),
...getDocumentLinksAst(context, indexEntry)
];
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;
}

function getGlossaryLinksAst(context, indexEntry, fromIndexFilename) {
return indexEntry.definitions.map((term, i) => {
const toGlossaryFilename = term.glossary.outPath;
const url = getFileLinkUrl(context, fromIndexFilename, toGlossaryFilename, term.anchor);
return link(url, term.getShortDescription(), text(term.glossary.title));
});
}

function getDocumentLinksAst(context, indexEntry) {
return Object.keys(indexEntry.occurrences).map((ref) => {
const {headingNode} = indexEntry.occurrences[ref];
const linkText = getNodeText(headingNode);
return link(ref, null, text(linkText));
});
}

/**
* Returns the URL for the section heading preceding a term occurrence.
*
* @param {*} context
* @param {string} filenameFrom path
* @param {string} filenameTo path
* @param {string} anchor optional anchor or url fragment for references to sections
*/
function getFileLinkUrl(context, filenameFrom, filenameTo, anchor) {
const {outDir, baseUrl, linking, generateFiles} = context.opts;
let targetUrl = "";
if (linking === 'relative') {
targetUrl = toForwardSlash(
relativeFromTo(
path.resolve(outDir, filenameFrom || "."),
path.resolve(outDir, filenameTo)
)
) + anchor;
} else if (linking === 'absolute') {
if (baseUrl) {
targetUrl = toForwardSlash(path.resolve(outDir, filenameFrom))
.replace(outDir, baseUrl)
.replace(/^(.*)(\/|\\)$/, "$1")
+ anchor;
} else {
targetUrl = toForwardSlash(path.resolve(outDir, filenameFrom))
+ anchor;
}
} else {
targetUrl = anchor;
}
return url.parse(targetUrl).format();
}

module.exports = { indexer, getAST };
Loading

0 comments on commit e9485f2

Please sign in to comment.