UNPKG

doctoc

Version:

Generates TOC for markdown files of local git repo.

221 lines (179 loc) 7.13 kB
"use strict"; var anchor = require("anchor-markdown-header"), updateSection = require("update-section"), getHtmlHeaders = require("./get-html-headers"), contentGenerator = require("./content-generation"), md = require("@textlint/markdown-to-ast"); function matchesStart(syntax) { var commentEscapedStart = contentGenerator.escapedStartTag(syntax); return function(line){ return new RegExp(`${commentEscapedStart} START doctoc `).test(line); } } function matchesEnd(syntax) { var commentEscapedStart = contentGenerator.escapedStartTag(syntax); return function(line){ return new RegExp(`${commentEscapedStart} END doctoc `).test(line); } } function notNull(x) { return x !== null; } function isString(y) { return typeof y === 'string'; } function generateMarkers(lines, info, pragmaStyle, syntax){ var start, end, header; if (pragmaStyle == "compact" && info.hasStart) { start = lines[info.startIdx]; start = start.replace(" generated TOC please keep comment here to allow auto update", ""); end = lines[info.endIdx]; end = end.replace(" generated TOC please keep comment here to allow auto update", ""); } else { var marker = contentGenerator.pragmaMarkers(syntax, pragmaStyle); start = marker.start; end = marker.end; } return { start, end }; } function getMarkdownHeaders (lines, maxHeaderLevel, minHeaderLevel) { function extractText (header) { return header.children .map(function (x) { if (x.type === md.Syntax.Link || x.type === md.Syntax.LinkReference) { return extractText(x); } else if (x.type === md.Syntax.Image || x.type === md.Syntax.ImageReference) { // Images (at least on GitHub, untested elsewhere) are given a hyphen // in the slug. We can achieve this behavior by adding an '*' to the // TOC entry. Think of it as a "magic char" that represents the image. return '*'; } else { return x.raw; } }) .join(""); } return md .parse(lines.join("\n")) .children.filter(function (x) { return x.type === md.Syntax.Header; }) .map(function (x) { return x.depth >= minHeaderLevel && (!maxHeaderLevel || x.depth <= maxHeaderLevel) ? { rank : x.depth , name : extractText(x) , line : x.loc.start.line } : null; }) .filter(notNull); } function processHeaders(headers, mode) { var instances = {}; headers.sort(function (a, b) { return a.line - b.line; }); for (var i = 0; i < headers.length; i++) { var header = headers[i]; var anchorLink = anchor(header.name, mode, 0, header.href); var name = header.href || anchorLink.split('#')[1].slice(0, -1); if (header.href === undefined && Object.prototype.hasOwnProperty.call(instances, name)) { // `instances.hasOwnProperty(name)` fails when there’s an instance named "hasOwnProperty". instances[name]++; } else { instances[name] = 0; } header.anchor = instances[name] > 0 ? anchor(header.name, mode, instances[name], header.href) : anchorLink; } return headers; } function getLinesToToc(lines, currentToc, info, processAll) { if (processAll || !currentToc) return lines; var tocableStart = 0; // when updating an existing toc, we only take the headers into account // that are below the existing toc if (info.hasEnd) tocableStart = info.endIdx + 1; return lines.slice(tocableStart); } // Use document context as well as command line args to infer the title function determineTitle(title, notitle, lines, info) { var defaultTitle = '**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*'; if (notitle) return ""; if (title) return title; if (!info.hasStart) return defaultTitle; var readTitle = lines[info.startIdx + 2]; var previousLine = lines[info.startIdx + 1]; return readTitle != "" || previousLine.includes("END doctoc") ? readTitle : previousLine; } exports = module.exports = function transform(content, mode, maxHeaderLevel, minHeaderLevel, minTocItems, title, notitle, entryPrefix, processAll, updateOnly, syntax, options) { syntax = syntax || "md"; var skipTag = contentGenerator.skipTag(syntax) + '\n'; var matchesStartBySyntax = matchesStart(syntax); var matchesEndBySyntax = matchesEnd(syntax); var index = content.indexOf(skipTag); if (index === 0 || (index >= 0 && content[index-1] === '\n')) return { transformed: false }; mode = mode || "github.com"; entryPrefix = entryPrefix || "-"; minHeaderLevel = minHeaderLevel || 1; if (isNaN(minTocItems) || minTocItems < 1){ minTocItems = 1; } var padTitle = options?.toc?.title?.padding?.before > 0; if (options?.toc?.title?.padding?.before === undefined){ padTitle = notitle || false; } // only limit *HTML* headings by default var maxHeaderLevelHtml = maxHeaderLevel || 4; var lines = content.split('\n'), info = updateSection.parse(lines, matchesStartBySyntax, matchesEndBySyntax); if (!info.hasStart && updateOnly) { return { transformed: false }; } var { start, end } = generateMarkers(lines, info, options?.toc?.pragma?.style, syntax); if (options?.toc?.header?.content) { start = start + '\n' + options.toc.header.content; } if (options?.toc?.footer?.content) { end = options.toc.footer.content + '\n' + end; } var inferredTitle = determineTitle(title, notitle, lines, info); var titleSeparator = inferredTitle ? '\n\n' : '\n'; var titlePadding = padTitle && inferredTitle ? '\n' : ''; var currentToc = info.hasStart && lines.slice(info.startIdx, info.endIdx + 1).join('\n'), linesToToc = getLinesToToc(lines, currentToc, info, processAll); var headers = getMarkdownHeaders(linesToToc, maxHeaderLevel, minHeaderLevel) .concat(getHtmlHeaders(linesToToc, maxHeaderLevelHtml, minHeaderLevel)); var allHeaders = processHeaders(headers, mode), lowestRank = allHeaders.reduce((min, h) => Math.min(min, h.rank), Infinity); var toc = ''; var wrappedToc; if (allHeaders.length >= minTocItems) { var indentation = ' '; var indentationWidth = options?.toc?.items?.indentation?.width; // remove this fallback based on mode in v3 if(indentationWidth === undefined){ // 4 spaces required for proper indention on Bitbucket and GitLab indentationWidth = (mode === 'bitbucket.org' || mode === 'gitlab.com') ? 4 : 2; } toc = titlePadding + inferredTitle + titleSeparator + allHeaders .map(function (x) { return indentation.repeat((x.rank - lowestRank) * indentationWidth) + entryPrefix + ' ' + x.anchor; }) .join('\n') + '\n'; wrappedToc = start + '\n' + toc + '\n' + end; } else { wrappedToc = start + '\n' + end; } var data; if (currentToc != wrappedToc){ data = updateSection(lines.join('\n'), wrappedToc, matchesStartBySyntax, matchesEndBySyntax, true); } return { transformed : currentToc != wrappedToc, data : data, toc: toc, wrappedToc: wrappedToc }; };