Skip to content

Commit

Permalink
Refactor to externalise toc generation to mdast-util-toc
Browse files Browse the repository at this point in the history
Closes GH-6.
BarryThePenguin authored and wooorm committed Jul 24, 2016
1 parent 61e924c commit dd6bf14
Showing 7 changed files with 85 additions and 279 deletions.
288 changes: 10 additions & 278 deletions index.js
Original file line number Diff line number Diff line change
@@ -15,286 +15,14 @@
*/

var slug = require('remark-slug');
var toString = require('mdast-util-to-string');
var toc = require('mdast-util-toc');

/*
* Constants.
*/

var HEADING = 'heading';
var LIST = 'list';
var LIST_ITEM = 'listItem';
var PARAGRAPH = 'paragraph';
var LINK = 'link';
var TEXT = 'text';
var DEFAULT_HEADING = 'toc|table[ -]of[ -]contents?';

/**
* Transform a string into an applicable expression.
*
* @param {string} value - Content to expressionise.
* @return {RegExp} - Expression from `value`.
*/
function toExpression(value) {
return new RegExp('^(' + value + ')$', 'i');
}

/**
* Check if `node` is the main heading.
*
* @param {Node} node - Node to check.
* @param {number} depth - Depth to check.
* @param {RegExp} expression - Expression to check.
* @return {boolean} - Whether `node` is a main heading.
*/
function isOpeningHeading(node, depth, expression) {
return depth === null && node && node.type === HEADING &&
expression.test(toString(node));
}

/**
* Check if `node` is the next heading.
*
* @param {Node} node - Node to check.
* @param {number} depth - Depth of opening heading.
* @return {boolean} - Whether znode is a closing heading.
*/
function isClosingHeading(node, depth) {
return depth && node && node.type === HEADING && node.depth <= depth;
}

/**
* Search a node for a location.
*
* @param {Node} root - Parent to search in.
* @param {RegExp} expression - Heading-content to search
* for.
* @param {number} maxDepth - Maximum-depth to include.
* @return {Object} - Results.
*/
function search(root, expression, maxDepth) {
var index = -1;
var length = root.children.length;
var depth = null;
var lookingForToc = true;
var map = [];
var child;
var headingIndex;
var closingIndex;
var value;

while (++index < length) {
child = root.children[index];

if (child.type !== HEADING) {
continue;
}

value = toString(child);

if (lookingForToc) {
if (isClosingHeading(child, depth)) {
closingIndex = index;
lookingForToc = false;
}

if (isOpeningHeading(child, depth, expression)) {
headingIndex = index + 1;
depth = child.depth;
}
}

if (!lookingForToc && value && child.depth <= maxDepth) {
map.push({
'depth': child.depth,
'value': value,
'id': child.data.htmlAttributes.id
});
}
}

if (headingIndex) {
if (!closingIndex) {
closingIndex = length + 1;
}

/*
* Remove current TOC.
*/

root.children.splice(headingIndex, closingIndex - headingIndex);
}

return {
'index': headingIndex || null,
'map': map
};
}

/**
* Create a list.
*
* @return {Object} - List node.
*/
function list() {
return {
'type': LIST,
'ordered': false,
'children': []
};
}

/**
* Create a list item.
*
* @return {Object} - List-item node.
*/
function listItem() {
return {
'type': LIST_ITEM,
'loose': false,
'children': []
};
}

/**
* Insert a `node` into a `parent`.
*
* @param {Object} node - `node` to insert.
* @param {Object} parent - Parent of `node`.
* @param {boolean?} [tight] - Prefer tight list-items.
*/
function insert(node, parent, tight) {
var children = parent.children;
var length = children.length;
var last = children[length - 1];
var isLoose = false;
var index;
var item;

if (node.depth === 1) {
item = listItem();

item.children.push({
'type': PARAGRAPH,
'children': [
{
'type': LINK,
'title': null,
'url': '#' + node.id,
'children': [
{
'type': TEXT,
'value': node.value
}
]
}
]
});

children.push(item);
} else if (last && last.type === LIST_ITEM) {
insert(node, last, tight);
} else if (last && last.type === LIST) {
node.depth--;

insert(node, last);
} else if (parent.type === LIST) {
item = listItem();

insert(node, item);

children.push(item);
} else {
item = list();
node.depth--;

insert(node, item);

children.push(item);
}

/*
* Properly style list-items with new lines.
*/

if (parent.type === LIST_ITEM) {
parent.loose = tight ? false : children.length > 1;
} else {
if (tight) {
isLoose = false;
} else {
index = -1;

while (++index < length) {
if (children[index].loose) {
isLoose = true;

break;
}
}
}

index = -1;

while (++index < length) {
children[index].loose = isLoose;
}
}
}

/**
* Transform a list of heading objects to a markdown list.
*
* @param {Array.<Object>} map - Heading-map to insert.
* @param {boolean?} [tight] - Prefer tight list-items.
* @return {Object} - List node.
*/
function contents(map, tight) {
var minDepth = Infinity;
var index = -1;
var length = map.length;
var table;

/*
* Find minimum depth.
*/

while (++index < length) {
if (map[index].depth < minDepth) {
minDepth = map[index].depth;
}
}

/*
* Normalize depth.
*/

index = -1;

while (++index < length) {
map[index].depth -= minDepth - 1;
}

/*
* Construct the main list.
*/

table = list();

/*
* Add TOC to list.
*/

index = -1;

while (++index < length) {
insert(map[index], table, tight);
}

return table;
}

/**
* Attacher.
*
@@ -304,7 +32,7 @@ function contents(map, tight) {
*/
function attacher(processor, options) {
var settings = options || {};
var heading = toExpression(settings.heading || DEFAULT_HEADING);
var heading = settings.heading || DEFAULT_HEADING;
var depth = settings.maxDepth || 6;
var tight = settings.tight;

@@ -317,9 +45,13 @@ function attacher(processor, options) {
* @param {Node} node - Root to search in.
*/
function transformer(node) {
var result = search(node, heading, depth);
var result = toc(node, {
'heading': heading,
'maxDepth': depth,
'tight': tight
});

if (result.index === null || !result.map.length) {
if (result.index === null || result.index === -1 || !result.map) {
return;
}

@@ -329,8 +61,8 @@ function attacher(processor, options) {

node.children = [].concat(
node.children.slice(0, result.index),
contents(result.map, tight),
node.children.slice(result.index)
result.map,
node.children.slice(result.endIndex)
);
}

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@
],
"dependencies": {
"remark-slug": "^4.0.0",
"mdast-util-to-string": "^1.0.0"
"mdast-util-toc": "^2.0.0"
},
"repository": {
"type": "git",
2 changes: 2 additions & 0 deletions test/fixtures/missing-content/output.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Missing content

## Table of Contents

* * *
15 changes: 15 additions & 0 deletions test/fixtures/normal-content/input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Normal

## Table of Contents

Here’s some content.

# Something if

## Something else

Text.

## Something elsefi

# Something iffi
20 changes: 20 additions & 0 deletions test/fixtures/normal-content/output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Normal

## Table of Contents

- [Something if](#something-if)

- [Something else](#something-else)
- [Something elsefi](#something-elsefi)

- [Something iffi](#something-iffi)

# Something if

## Something else

Text.

## Something elsefi

# Something iffi
17 changes: 17 additions & 0 deletions test/fixtures/normal-more-content/input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Normal

## Table of Contents

Here’s some content.

Here’s some more content.

# Something if

## Something else

Text.

## Something elsefi

# Something iffi
20 changes: 20 additions & 0 deletions test/fixtures/normal-more-content/output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Normal

## Table of Contents

- [Something if](#something-if)

- [Something else](#something-else)
- [Something elsefi](#something-elsefi)

- [Something iffi](#something-iffi)

# Something if

## Something else

Text.

## Something elsefi

# Something iffi

0 comments on commit dd6bf14

Please sign in to comment.