diff --git a/.github/workflows/push-staging-packager.yml b/.github/workflows/push-staging-packager.yml index d8c3d78..9a2a87f 100644 --- a/.github/workflows/push-staging-packager.yml +++ b/.github/workflows/push-staging-packager.yml @@ -3,6 +3,9 @@ name: Staging Packager on: push: branches: ['pub-pre-release'] + release: + types: + - created jobs: build-test-package: @@ -31,8 +34,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: markdowntoc-package - path: jr-markdowntoc-vscode-?.?.?.vsix + path: jr-markdowntoc-vscode-0.4.0.vsix if-no-files-found: 'error' - name: publish to marketplace + if: success() run: | vsce publish -p ${{ secrets.VSCE_PAT}} --pre-release diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..ae7ce53 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.fontFamily": "Consolas, 'Courier New', monospace", + "editor.fontSize": 18, + "editor.lineHeight": 1.4 +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 08322d0..0b87555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,50 @@ # Change Log -All notable changes to the "markdown-toc" extension will be documented in this file, using Sematic Versioning. +All notable changes to the "markdown-toc" extension will be documented in this file using Sematic Versioning. See [Keep a Changelog](http://keepachangelog.com/) for how this file is structured. ## [Unreleased] +## [0.4.0] - 2024-09-24 + +Added: + +- Support for Closed ATX Style Headings. + +```markdown +# Top Level Heading # + +Lorem ipsum... + +## Table of Contents ## + +- [Second Level Heading](#second-level-heading) + +## Second Level Heading ## + +Dolor sit amet... + +``` + +Bugfixes: + +- Generated Table of Contents should use same style as first discovered heading. +- Comma and period characters cause invalid link fragment generation. +- Slash character in a heading causes invalid link fragment generation. +- Undocumented: Space characters in Heading causes invalid link fragment generation. + +## [0.3.1] - 2024-07-22 + +Added: + +- Heading Text used in generatd Table of Contents will only be altered if unsupported characters are included. +- Multiple unit tests for better code coverage. + +Bugfixes: + +- Table of Contents created invalid link fragments in some cases. + ## [0.2.1] - 2024-07-01 Added: diff --git a/README.md b/README.md index 37d5ef6..3a75232 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,16 @@ Locates all Level 2 headings in the currently selected markdown file and creates ## Features -- Creates a table of contents in the currently open markdown file just prior to the 1st Level 2 heading. +- Creates a table of contents in the currently open markdown file just prior to the first Level 2 heading. - Links to Level 2 headings are stored in the generated table of contents. -- Supports standard Headings (prefixed with `#`) and alternate Headings syntax (`=` or `-` characters on nextline following heading title). -- Will try to put the generated Table of Contents near the top of the document if there is no Level 1 Heading. -- Attempts to be non-destructive but will strip-out Heading characters that link fragments do not support. -- Uses the VSCode Command Palette to insert the new table of contents. +- Supports standard Headings (Open ATX) that are only prefixed with `#` character(s). +- Support Closed ATX Headings prefixed and suffixed with `#` character(s). +- Supports alternate (Next Line) Headings syntax using `=` or `-` characters on nextline following heading title. +- Generated Table of Contents uses same style of heading (Open, closed, ATX, etc) that it detects within your document. +- If there is no first level heading, the generated Table of Contents will be inserted near the top of the document. +- Unsupported link-fragment characters are removed. +- Headings that contain characters other than alpha-numerics will likely be processed with the exception of unsupported characters such as `<`, `>`, and `:`. +- Use the VSCode Command Palette to insert the new table of contents. See [CHANGELOG.md](./CHANGELOG.md) for details. @@ -35,7 +39,17 @@ Select the command and a Table of Contents will be created with Heading IDs link ## Requirements -The only dependencies are `vscode ^1.90.0 and those listed in devDependencies in the package.json file in my [GitHub Project Repo](https://github.com/nojronatron/markdown-toc/). +Build: `npm install` + +Test: `npm test` + +Install: Open _Extensions_ panel (`ctrl+shift+x`) in VS Code and search for "Create Markdown TOC". + +For development: + +- `vscode ^1.90.0`. +- `node 20.14.x`. +- See `package.json` for additional dev dependencies in [my GitHub Project Repo](https://github.com/nojronatron/markdown-toc/). ## Extension Settings @@ -48,8 +62,9 @@ ActivationEvents: none. - Always use a Markdown Linter before running this tool for the best results. - A Level 1 Heading must be followed by a newline character or it will be ignored (note: Table of Contents generation does not depend on any Level 1 heading specifically). - Headings that start with a space may cause Create ToC to ignore the heading. -- Headings that contain characters other than alpha-numerics will likely be processed _but_ the generated Table of Contents link might not be active. - Skipped level 2 headings will not be shown in the generated Table of Contents. +- Using multiple headings types will produce unexpected results. +- A user can create a Heading with many spaces in it that will Lint without error. Create ToC will generate a valid, Linted Link Fragment for that "spacey heading" _but_ the link will be dead. _Note_: See [GitHub Issues List](https://github.com/nojronatron/markdown-toc/issues) for the most current status. diff --git a/extension-functions/create-toc.js b/extension-functions/create-toc.js index e775200..032d0fb 100644 --- a/extension-functions/create-toc.js +++ b/extension-functions/create-toc.js @@ -1,3 +1,7 @@ +const { getTitleOnly, + getLoweredKebabCase, + getLinkFragment } = require('./process-headings'); + /** * Generates a table of contents (TOC) based on the captured level 2 headings. * @@ -5,30 +9,32 @@ * @returns {string} The generated table of contents. */ module.exports = function createTOC(capturedL2Headings) { - let tableOfContentsHeading = 'Table of Contents\n'; - let tableOfContentsString = ''; + let tableOfContentsHeading = 'Table of Contents'; + let tocString = ''; + let tocPrefix = ''; + let tocSuffix = ''; if (capturedL2Headings[0].isHash) { - tableOfContentsString = `## ${tableOfContentsHeading}\n`; + tocPrefix = `## `; + + if (capturedL2Headings[0].isClosedAtx) { + tocSuffix = ` ##`; + } } else { - tableOfContentsString = `${tableOfContentsHeading}-----------------\n\n`; + tocSuffix = `\n-----------------`; } - + + tocString = `${tocPrefix}${tableOfContentsHeading}${tocSuffix}\n\n`; + capturedL2Headings.forEach((item) => { - // Extract the level2 heading text, removing - // characters not allowed in link fragments - const titleOnly = item.text.replaceAll(/(?:[!@$%^&*\(\)\[\]\{\}\:';\.,~`+=\\"\|\/?])/g, '') - .trim(); - // Convert the heading text to kebab case - const loweredKebabCase = item.text.toLowerCase() - .replace(/\s/g, '-'); - // compose the link fragement title and anchor element - const linkFragment = `- [${titleOnly}](#${loweredKebabCase})\n`; - // append the link fragment to the table of contents string - tableOfContentsString += linkFragment; + // call external modules to do this work + const titleOnly = getTitleOnly(item.text); + const loweredKebabCase = getLoweredKebabCase(titleOnly); + const linkFragment = getLinkFragment(titleOnly, loweredKebabCase); + tocString += linkFragment; }); // be kind, leave a blank line after the table of contents - tableOfContentsString += '\n'; - return tableOfContentsString; + tocString += '\n'; + return tocString; }; diff --git a/extension-functions/find-top-heading.js b/extension-functions/find-top-heading.js index e773180..0f6f0c5 100644 --- a/extension-functions/find-top-heading.js +++ b/extension-functions/find-top-heading.js @@ -9,30 +9,42 @@ * @property {boolean} isToc - Indicates whether the top heading is a table of contents. */ module.exports = function findTopHeading(document) { - const resultObj = { line: -1, text: '', isHash: true, isToc: false }; + const resultObj = { line: -1, text: '', isHash: false, isClosedAtx: false, isToc: false }; let lineIdx = 0; // 1. get index of first newline character let newlineCharIdx = document.indexOf('\n'); let previousText = ''; - // 6. continue until index of newline character is -1 + // 2. continue until index of newline character is -1 while (newlineCharIdx > -1) { // 3. substring from startIdx to index of newline character let currentText = document.substring(0, newlineCharIdx).trim(); - // 4a. if substring starts with '# ' and return with updated resultObj + // 4. check if substring is an open or closed ATX style heading if (currentText.startsWith('# ')) { resultObj.line = lineIdx; resultObj.text = currentText; + resultObj.isHash = true; + resultObj.isClosedAtx = false; + resultObj.isToc = false; + + // 4a. if substring starts with '# ' and ends with ' #' return an updated resultObj + if (currentText.endsWith(' #')) { + resultObj.isClosedAtx = true; + } + + // 4b. if substring starts with '# ' return an updated resultObj return resultObj; } - // 4b. if substring starts with '=' and previous line has text and return with updated resultObj + // 5. if substring starts with '=' and previous line has text return an updated resultObj if (currentText .startsWith('=')) { if (previousText.match(/^(?:[a-zA-Z0-9_] *?)+$/m)) { resultObj.line = lineIdx; resultObj.text = previousText; resultObj.isHash = false; + resultObj.isClosedAtx = false; + resultObj.isToc = false; return resultObj; } } diff --git a/extension-functions/get-second-level-heading.js b/extension-functions/get-second-level-heading.js index b9698b3..24eb89d 100644 --- a/extension-functions/get-second-level-heading.js +++ b/extension-functions/get-second-level-heading.js @@ -1,64 +1,115 @@ +const getTitleOnly = require('./process-headings').getTitleOnly; + /** * Retrieves the second level heading information from the given lines of text and heading style. * @param {string} firstLine - The first line of text. * @param {number} firstLineIdx - The index of the first line. * @param {string} secondLine - The second line of text. - * @param {boolean} [normalStyle=true] - Indicates whether the heading is in normal style (using '##') or alternative style (using '-'). + * @param {boolean} isHash - Indicates whether the heading uses normal style ('##') or alternate style ('-'). + * @param {boolean} isClosedAtx - Indicates whether the heading uses closed ATX style (using '#' character(s) at end of heading line). * @returns {object} - An object containing the heading information. * @property {number} line - The line index of the heading. * @property {string} text - The text of the heading. - * @property {boolean} isHash - Indicates whether the heading is in normal style (using '##') or alternative style (using '-'). + * @property {boolean} isHash - Indicates whether the heading uses normal style ('##') or alternate style ('-'). + * @property {boolean} isClosedAtx - Indicates whether the heading uses closed ATX style (using '#' character(s) at end of heading line). * @property {boolean} isToc - Indicates whether the heading is a table of contents. */ -function getSecondLevelHeading(firstLine, firstLineIdx, secondLine, normalStyle = true) { - if (normalStyle) { +function getSecondLevelHeading(firstLine, firstLineIdx, secondLine, isHash, isClosedAtx) { + // firstLIne: string of text, firstLineIdx: Line Num of text, secondLine: string of text, isHash: '#' (true) or '-' (false) is used, isClosedAtx: ' #' (true) or '' (false) is used + + // clean title of all illegal characters + let cleanedFirstLine = getTitleOnly(firstLine); + + if (isTableOfContents(cleanedFirstLine)) { + return { + line: firstLineIdx, + text: cleanedFirstLine, + isHash: isHash, + isClosedAtx: isClosedAtx, + isToc: true, + }; + } + + if (isHash) { return getHash2LH(firstLine, firstLineIdx, secondLine); } - if (!normalStyle) { + + if (!isHash && !isClosedAtx) { return getDash2LH(firstLine, firstLineIdx, secondLine); } // no matches so the line of text is something else - return { line: -1, text: firstLine, isHash: normalStyle, isToc: false }; + return { + line: -1, + text: cleanedFirstLine, + isHash: isHash, + isClosedAtx: isClosedAtx, + isToc: false + }; +} + +/** + * Check if the first line contains the text 'Table of Contents'. + * @param {string} firstLine the text of the first line + * @returns {boolean} true if the first line contains the text 'Table of Contents' + */ +function isTableOfContents(firstLine) { + // if 'table of contents' is found the matcher returns an array, otherwise null + return firstLine.match(/^.*?(?:Table of Contents).*?$/) !== null; } /** - * Get the second level heading information when using standard style headings. + * Get the second level heading details when using open ATX style headings. * @param {string} firstLine * @param {number} firstLineIdx * @param {string} secondLine - * @returns {object} {line, text, isHash, isToc} + * @returns {object} {line, text, isHash, isClosedAtx, isToc} */ -function getHash2LH(firstLine, firstLineIdx, secondLine) { +function getHash2LH(firstLine, firstLineIdx) { // This function will not consistently find L2 headings if either line begins with a space + + const resultObj = { + line: -1, + text: firstLine, + isHash: false, + isClosedAtx: false, + isToc: false, + }; + + // check for open ATX style if (firstLine.startsWith('## ')) { - if (firstLine.match(/^(?:## Table of Contents)\s*?$/m)) { - // do not count an existing table of contents - return { - line: firstLineIdx, - text: firstLine, - isHash: true, - isToc: true, - } + resultObj.isHash = true; + + // exclude leading hash character(s) from future substring + let newStartIdx = 0; + while(firstLine[newStartIdx] === '#' && newStartIdx < firstLine.length) { + newStartIdx++; } - if (secondLine.length === 0 - || secondLine.match(/^\s$/) - || secondLine.match(/^(?:[a-zA-Z0-9_] *?)+$/m)) { - const firstLineText = firstLine.substring(2, firstLine.length).trim(); - return { - line: firstLineIdx, - text: firstLineText, - isHash: true, - isToc: false, + // check for closed ATX style + let newEndIdx = firstLine.length - 1; + + if (firstLine.substring(firstLine.length - 1) === '#') { + resultObj.isClosedAtx = true; + + // exclude trailing hash character(s) from future substring + while(firstLine[newEndIdx] === '#' && newEndIdx >= newStartIdx) { + newEndIdx--; }; } + + // in JS substring() method use end index not length, and end idx is exclusive + const firstLineTextOnly = firstLine.substring(newStartIdx, ++newEndIdx).trim(); + resultObj.line = firstLineIdx; + resultObj.text = firstLineTextOnly; + return resultObj; } return { line: -1, text: firstLine, isHash: true, + isClosedAtx: false, isToc: false, }; } @@ -68,71 +119,32 @@ function getHash2LH(firstLine, firstLineIdx, secondLine) { * @param {string} firstLine * @param {number} firstLineIdx * @param {string} secondLine - * @returns {object} {line, text, isHash, isToc} + * @returns {object} {line, text, isHash, isClosedAtx, isToc} */ function getDash2LH(firstLine, firstLineIdx, secondLine) { - if (secondLine.startsWith('-')) { - if (firstLine.match(/^(?:Table of Contents)\s*?$/m)) { - // do not count an existing table of contents - return { - line: firstLineIdx, - text: firstLine, - isHash: false, - isToc: true, - } - } - - if (firstLine.match(/^(?:[a-zA-Z0-9_] *?)+$/m)) { - const firstLineText = firstLine.trim(); - return { - line: firstLineIdx, - text: firstLineText, - isHash: false, - isToc: false, - }; - } - } - - return { + const returnObj = { line: -1, text: firstLine, - isHash: true, + isHash: false, + isClosedAtx: false, isToc: false, }; -} -/** - * Iterate through a string document level two headings to find the existing style character. - * @param {string} document - * @returns {string} '#' or '-' or '' if not found - */ -function findExistingStyleCharacter(document) { - let newlineCharIdx = document.indexOf('\n'); - let previousText = ''; - - while (newlineCharIdx > -1) { - let currentText = document.substring(0, newlineCharIdx).trim(); - - if (currentText.startsWith('## ')) { - return '#'; - } - - if (currentText.startsWith('-') - && previousText.match(/^(?:[a-zA-Z0-9_] *?)+$/m)) { - return '-'; + // check for a dash style heading in line 2 + if (secondLine.startsWith('-')) { + // check for text in line 1 and clean it up for future use + if (firstLine.match(/^(?:[a-zA-Z0-9_] *?)+$/m)) { + const firstLineText = firstLine.trim(); + returnObj.line = firstLineIdx; + returnObj.text = firstLineText; } - - previousText = currentText; - document = document.substring(newlineCharIdx).trim(); - newlineCharIdx = document.indexOf('\n'); } - return ''; + return returnObj; } module.exports = { getSecondLevelHeading, getHash2LH, getDash2LH, - findExistingStyleCharacter, }; diff --git a/extension-functions/process-headings.js b/extension-functions/process-headings.js new file mode 100644 index 0000000..a9fbfe7 --- /dev/null +++ b/extension-functions/process-headings.js @@ -0,0 +1,50 @@ +/** + * Extracts the level2 heading text and removes '<', '>', and ':' characters. + * + * @param {string} itemText - The input text to process. + * @returns {string} The processed and trimmed title. + */ +const getTitleOnly = function(itemText) { +// Extract the level2 heading text +if (itemText.length < 1) { + return 'No Heading Text'; + } + + const cleanedItemText = itemText.replaceAll(/<|>|:/g, ''); + const trimmedTitle = cleanedItemText.trim(); + return trimmedTitle; +} + +/** + * Removes illegal characters and returns the Heading Text to lower-kebab-case string. + * + * @param {string} itemText - The input Heading Text. + * @returns {string} The lower-kebab-case string. + */ +const getLoweredKebabCase = function(itemText) { + // Remove illegal characters, replace spaces with dashes, and convert the input text to lower-kebab-case + const cleanedItemText = itemText.replaceAll(/(?:[#!@\$%\^&*'";,~`\.\+=\(\)\[\]\{\}\<\>\:\\\|\/\?])/g, ''); + const trimmedItemText = cleanedItemText.trim(); + const loweredcase = trimmedItemText.toLowerCase(); + const loweredKebabCase = loweredcase.replaceAll(/\s/g, '-'); + return loweredKebabCase; +} + +/** + * Generates a link fragment for a given title and lowered kebab case string. + * + * @param {string} titleOnlyString - The title of the link. + * @param {string} loweredKebabCaseString - The lowered kebab case string used as the link fragment. + * @returns {string} The generated link fragment. + */ +const getLinkFragment = function(titleOnlyString, loweredKebabCaseString) { + // Concatenate the strings Title and LoweredKebabCase to a valid link fragment + const linkFragment = `- [${titleOnlyString}](#${loweredKebabCaseString})\n`; + return linkFragment; +} + +module.exports = { + getTitleOnly, + getLoweredKebabCase, + getLinkFragment +} diff --git a/extension.js b/extension.js index aea6850..468d4e0 100644 --- a/extension.js +++ b/extension.js @@ -1,6 +1,6 @@ const vscode = require('vscode'); const findTopHeading = require('./extension-functions/find-top-heading'); -const {getSecondLevelHeading, findExistingStyleCharacter} = require('./extension-functions/get-second-level-heading'); +const {getSecondLevelHeading} = require('./extension-functions/get-second-level-heading'); const createTOC = require('./extension-functions/create-toc'); /** @@ -16,40 +16,32 @@ function activate(context) { async function () { const firstCharacter = 0; const editor = vscode.window.activeTextEditor; - let topHeading = { line: -1, text: '', isHash: true, isToc: false }; + let topHeading = { line: -1, text: '', isHash: true, isClosedAtx: false, isToc: false }; if (editor && editor.document.languageId === 'markdown') { - // 1. Find the top heading of the page and store in a plain - // obj with parameters Text, Line Number, and Hash true/false. + + if (editor.document.lineCount < 2) + { + vscode.window.showWarningMessage('Add headings to the document and try again.'); + return null; + } + + // 1. Find the top heading of the page, store in an object + // 2. determine whether to use hash (openAtx or ClosedAtx) or dash (alternate) headings style topHeading = findTopHeading(editor.document.getText()); - if (topHeading.line < 0) { + if (!topHeading.line === -1) { vscode.window.showWarningMessage( 'No top level headings found.' ); - // 2. determine whether to use hash or dash for headings style - let foundCharacter = findExistingStyleCharacter(editor.document.getText()); - - switch (foundCharacter) { - case '#': - topHeading.isHash = true; - break; - case '-': - topHeading.isHash = false; - break; - default: - vscode.window.showWarningMessage( - 'No heading style found. Add a heading and try again.' - ); - return null; - } + return null; } // Check if there is enough content to add a TOC if (editor.document.lineCount <= topHeading.line + 3) { vscode.window.showWarningMessage( - 'Add more content below first level and try again.' + 'Add more content below first level heading and try again.' ); return null; @@ -58,27 +50,28 @@ function activate(context) { // 3. iterate over the document to find and store all second level headings const secondLevelHeadings = []; let start = topHeading.line > -1 ? topHeading.line + 3 : 1; + for (let idx = start; idx < editor.document.lineCount; idx++) { let firstLine = editor.document.lineAt(idx-1).text; let secondLine = editor.document.lineAt(idx).text; - const headingObject = getSecondLevelHeading(firstLine, idx-1, secondLine, topHeading.isHash); + const headingObject = getSecondLevelHeading(firstLine, idx-1, secondLine, topHeading.isHash, topHeading.isClosedAtx); if (headingObject.isToc) { // do no harm to existing document and allow user to make changes vscode.window.showWarningMessage('Table of Contents already exists.'); return null; } - + if (headingObject.line !== -1) { - // add the heading object to the array + // add the heading object to the array if line is not -1 secondLevelHeadings.push(headingObject); } } - // no second level headings? no table of contents to create + // no second level headings? no table of contents to create, give control back to user without editing if (secondLevelHeadings.length < 1) { vscode.window.showWarningMessage( - 'No second level headings to reference.' + 'No second level headings found.' ); return null; } diff --git a/package-lock.json b/package-lock.json index 7cabd67..b86688f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jr-markdowntoc-vscode", - "version": "0.2.1", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jr-markdowntoc-vscode", - "version": "0.2.1", + "version": "0.3.1", "dependencies": { "latest": "^0.2.0" }, @@ -24,7 +24,7 @@ "typescript": "^5.1.3" }, "engines": { - "vscode": "^1.79.0" + "vscode": "^1.90.0" } }, "node_modules/@azure/abort-controller": { diff --git a/package.json b/package.json index f954051..b256678 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "type": "git", "url": "https://github.com/nojronatron/markdown-toc.git" }, - "version": "0.3.1", + "version": "0.4.0", "author": "Jon Rumsey", "pricing": "Free", "engines": { diff --git a/test/suite/createTOC.test.js b/test/suite/createTOC.test.js index 11b0a5b..81058f8 100644 --- a/test/suite/createTOC.test.js +++ b/test/suite/createTOC.test.js @@ -18,6 +18,23 @@ suite('createTOC Module tests', () => { assert.strictEqual(actualToc1, expectedToc1); }); + test('createTOC generates valid link fragments even when unexpected non-alpha characters are included', () => { + const hashTocHeading = '## Table of Contents\n\n'; + const useHashCharacter = true; + + const headings1a = {line: 4, text: 'Items / Todos', isHash: useHashCharacter, isToc: false }; + const headings1b = {line: 8, text: 'Dotnet.net', isHash: useHashCharacter, isToc: false }; + const headings1c = {line: 12, text: 'Aspnetcore, Blazor', isHash: useHashCharacter, isToc: false }; + const headings1d = {line: 16, text: 'Underscore _lowercase Character', isHash: useHashCharacter, isToc: false }; + + const expectedToc1 = hashTocHeading + '- [Items / Todos](#items--todos)\n- [Dotnet.net](#dotnetnet)\n- [Aspnetcore, Blazor](#aspnetcore-blazor)\n- [Underscore _lowercase Character](#underscore-_lowercase-character)\n\n'; + + const arrayOfHeadings1 = [headings1a, headings1b, headings1c, headings1d]; + const actualToc1 = createTOC(arrayOfHeadings1); + + assert.strictEqual(actualToc1, expectedToc1, 'The actual Link Fragments do not match the expected Link Fragments'); + }); + test('createTOC generates a table of contents based on an array of headings and the alternate heading style character', ()=>{ const dashTocHeading = 'Table of Contents\n-----------------\n\n'; const useHashCharacter = false; // use dash character diff --git a/test/suite/findTopHeading.test.js b/test/suite/findTopHeading.test.js index 1de80ec..59b69dc 100644 --- a/test/suite/findTopHeading.test.js +++ b/test/suite/findTopHeading.test.js @@ -2,6 +2,17 @@ const assert = require('assert'); const findTopHeading = require('../../extension-functions/find-top-heading'); suite('findTopHeading tests', () => { + test('should find Closed ATX style level 1 heading in a markdown document', () => { + const markdown = '# Heading 1 Match #\n# No Match Heading 1\n## Heading 2 No Match\n## Heading 2 Closed ATX No Match\n'; + const expected = { line: 0, text: '# Heading 1 Match #', isHash: true, isClosedAtx: true, isToc: false }; + const actual = findTopHeading(markdown); + assert.strictEqual(actual.text, expected.text, 'The text of the top heading does not match the expected text'); + assert.strictEqual(actual.line, expected.line, 'The line number of the top heading does not match the expected line number'); + assert.strictEqual(actual.isHash, expected.isHash, 'The top heading does not start with a hash character'); + assert.strictEqual(actual.isClosedAtx, expected.isClosedAtx, 'The top heading is not in closed ATX style'); + assert.strictEqual(actual.isToc, expected.isToc, 'The top heading is a table of contents'); + }); + test('should return the correct first level heading using standard heading style in a markdown document', () => { const markdown = '# Heading 1\n## Heading 2\n### Heading 3\n'; const expected = { line: 0, text: '# Heading 1', isHash: true, isToc: false }; @@ -64,14 +75,14 @@ suite('findTopHeading tests', () => { }); test('should not find a first level heading if one does not exist in the markdown', () => { - const expected = { line: -1, text: '', isHash: true, isToc: false }; + const expected = { line: -1, text: '', isHash: false, isToc: false }; const markdown1 = 'Heading 1\n-\n\ntext\n\nHeading2\n--\n\ntext\n\n'; const actual1 = findTopHeading(markdown1); - assert.strictEqual(actual1.line, expected.line); - assert.strictEqual(actual1.text, expected.text); - assert.strictEqual(actual1.isHash, expected.isHash); - assert.strictEqual(actual1.isToc, expected.isToc); + assert.strictEqual(actual1.line, expected.line, 'The line number of the top heading does not match the expected line number'); + assert.strictEqual(actual1.text, expected.text, 'The text of the top heading does not match the expected text'); + assert.strictEqual(actual1.isHash, expected.isHash, 'The top heading does not start with a hash character'); + assert.strictEqual(actual1.isToc, expected.isToc, 'The top heading is a table of contents'); const markdown2 = '## Heading 1\ntext\n## Heading2\ntext\n\n## Heading 3\n\ntext\n\n\n'; const actual2 = findTopHeading(markdown2); diff --git a/test/suite/getSecondLevelHeadings.test.js b/test/suite/getSecondLevelHeadings.test.js index 0cd6962..40a8822 100644 --- a/test/suite/getSecondLevelHeadings.test.js +++ b/test/suite/getSecondLevelHeadings.test.js @@ -1,158 +1,53 @@ const assert = require('assert'); -const { getSecondLevelHeading, getHash2LH, getDash2LH, findExistingStyleCharacter } = require('../../extension-functions/get-second-level-heading'); +const { getSecondLevelHeading, getHash2LH, getDash2LH } = require('../../extension-functions/get-second-level-heading'); suite('getSecondLevelHeading Module tests', () => { - test('getDash2LH returns alternate style L2 heading line, text, and isHash false', () => { - const firstLine1 = 'Heading 1'; - const secondLine1 = '-'; - const firstLineIdx1 = 2; - // the standard heading style text will always be trimmed to just the text, no hashmarks - const expected1 = { line: 2, text: 'Heading 1', isHash: false, isToc: false }; - - const actual1 = getDash2LH(firstLine1, firstLineIdx1, secondLine1); - assert.strictEqual(actual1.line, expected1.line); - assert.strictEqual(actual1.text, expected1.text); - assert.strictEqual(actual1.isHash, expected1.isHash); - assert.strictEqual(actual1.isToc, expected1.isToc); - - const firstLine2 = 'Heading 2\n'; - const secondLine2 = '---------'; - const firstLineIdx2 = 4; - const expected2 = { line: 4, text: 'Heading 2', isHash: false, isToc: false }; - - const actual2 = getDash2LH(firstLine2, firstLineIdx2, secondLine2); - assert.strictEqual(actual2.line, expected2.line); - assert.strictEqual(actual2.text, expected2.text); - assert.strictEqual(actual2.isHash, expected2.isHash); - assert.strictEqual(actual2.isToc, expected2.isToc); - - const firstLine3 = 'Heading 3 '; - const secondLine3 = '------- -\n'; - const firstLineIdx3 = 6; - const expected3 = { line: 6, text: 'Heading 3', isHash: false, isToc: false }; - - const actual3 = getDash2LH(firstLine3, firstLineIdx3, secondLine3); - assert.strictEqual(actual3.line, expected3.line); - assert.strictEqual(actual3.text, expected3.text); - assert.strictEqual(actual3.isHash, expected3.isHash); - assert.strictEqual(actual3.isToc, expected3.isToc); - - const firstLine4 = 'Table of Contents'; - const secondLine4 = '-----------------\n'; - const firstLineIdx4 = 5; - const expected4 = { line: 5, text: 'Table of Contents', isHash: false, isToc: true }; - - const actual4 = getDash2LH(firstLine4, firstLineIdx4, secondLine4); - assert.strictEqual(actual4.line, expected4.line); - assert.strictEqual(actual4.text, expected4.text); - assert.strictEqual(actual4.isHash, expected4.isHash); - assert.strictEqual(actual4.isToc, expected4.isToc); - }); - - test('getHash2LH returns standard style L2 heading line, text, and isHash true', () => { - const firstLine1 = '## Heading 1'; - const secondLine1 = ''; - const firstLineIdx1 = 2; - // the standard heading style text will always be trimmed to just the text, no hashmarks - const expected1 = { line: 2, text: 'Heading 1', isHash: true, isToc: false }; - - const actual1 = getHash2LH(firstLine1, firstLineIdx1, secondLine1); - assert.strictEqual(actual1.line, expected1.line); - assert.strictEqual(actual1.text, expected1.text); - assert.strictEqual(actual1.isHash, expected1.isHash); - assert.strictEqual(actual1.isToc, expected1.isToc); - - const firstLine2 = '## Heading 2\n'; - const secondLine2 = 'text\n'; - const firstLineIdx2 = 4; - const expected2 = { line: 4, text: 'Heading 2', isHash: true, isToc: false }; - - const actual2 = getHash2LH(firstLine2, firstLineIdx2, secondLine2); - assert.strictEqual(actual2.line, expected2.line); - assert.strictEqual(actual2.text, expected2.text); - assert.strictEqual(actual2.isHash, expected2.isHash); - assert.strictEqual(actual2.isToc, expected2.isToc); + // standard line constants for the getSecondLevelHeading function tests + const standardLine1 = '# Top Level Heading\n'; + const standardLine2 = '## Level 2 Heading\n'; + const standardLine3 = '### Level 3 Heading\n'; - const firstLine3 = '## Heading 3 '; - const secondLine3 = 'text\n'; - const firstLineIdx3 = 6; - const expected3 = { line: 6, text: 'Heading 3', isHash: true, isToc: false }; - - const actual3 = getHash2LH(firstLine3, firstLineIdx3, secondLine3); - assert.strictEqual(actual3.line, expected3.line); - assert.strictEqual(actual3.text, expected3.text); - assert.strictEqual(actual3.isHash, expected3.isHash); - assert.strictEqual(actual3.isToc, expected3.isToc); + const lineMsg = 'Line number does not match.'; + const textMsg = 'Text does not match.'; + const isHashMsg = 'isHash does not match.'; + const isClosedAtxMsg = 'isClosedAtx does not match.'; + const isTocMsg = 'isToc does not match.'; - - const firstLine4 = '## Table of Contents'; - const secondLine4 = ' '; - const firstLineIdx4 = 4; - const expected4 = { line: 4, text: '## Table of Contents', isHash: true, isToc: true }; - - const actual4 = getHash2LH(firstLine4, firstLineIdx4, secondLine4); - assert.strictEqual(actual4.line, expected4.line); - assert.strictEqual(actual4.text, expected4.text); - assert.strictEqual(actual4.isHash, expected4.isHash); - assert.strictEqual(actual4.isToc, expected4.isToc); + test('getSecondLevelHeading returns correct object when standard style L1 heading is passed-in', () => { + const expectedStandard1 = { line: -1, text: '# Top Level Heading\n', isHash: true, isClosedAtx: false, isToc: false }; // the newline character is included in the text + const actualResult1 = getSecondLevelHeading(standardLine1, 0, ' ', true, false); + assert.strictEqual(actualResult1.line, expectedStandard1.line, lineMsg); + assert.strictEqual(actualResult1.text, expectedStandard1.text, textMsg); + assert.strictEqual(actualResult1.isHash, expectedStandard1.isHash, isHashMsg); + assert.strictEqual(actualResult1.isClosedAtx, false, isClosedAtxMsg); + assert.strictEqual(actualResult1.isToc, expectedStandard1.isToc, isTocMsg); }); - test('findExistingStyleCharacter returns the first character discovered (hash or dash), or a space character if neither could be found', ()=>{ - // Raw lorem ipsum text generated by [Lipsum.com](https://www.lipsum.com) - - const markdown1 = '# Top Heading\n\nLorem ipsum dolor sit amet, \nconsectetur adipiscing elit. \nPellentesque hendrerit tempus massa. \nCurabitur consectetur mollis scelerisque. \n\n## Heading 2\n\nUt at suscipit enim. \nSed convallis varius urna, \nquis scelerisque nisl facilisis at. \n'; - const expected1 = '#'; - const actual1 = findExistingStyleCharacter(markdown1); - assert.strictEqual(actual1, expected1); - - const markdown2 = 'Top Heading\n=\n\nLorem ipsum dolor sit amet, \nconsectetur adipiscing elit. \nPellentesque hendrerit tempus massa. \nCurabitur consectetur mollis scelerisque. \n\nHeading 2\n-\n\nUt at suscipit enim. \nSed convallis varius urna, \nquis scelerisque nisl facilisis at. \n'; - const expected2 = '-'; - const actual2 = findExistingStyleCharacter(markdown2); - assert.strictEqual(actual2, expected2); - - const markdown3 = 'Lorem ipsum dolor sit amet, \nconsectetur adipiscing elit. \nPellentesque hendrerit tempus massa. \nCurabitur consectetur mollis scelerisque. \nUt at suscipit enim. \nSed convallis varius urna, \nquis scelerisque nisl facilisis at. \nQuisque semper massa nec lobortis varius. \nNulla feugiat tristique ante, in bibendum lacus ornare id. \nNulla nec suscipit turpis. \nEtiam non mollis velit. \nNullam vulputate feugiat dui, \nsed suscipit dui consectetur ut. \nNulla facilisi. \nQuisque ut neque iaculis, \nhendrerit urna sed, \nrhoncus risus. \nDuis ac nulla sodales justo vestibulum aliquam a eget mi.'; - const expected3 = ''; - const actual3 = findExistingStyleCharacter(markdown3); - assert.strictEqual(actual3, expected3); - - const markdown4 = 'Heading 1\n\nLorem ipsum dolor sit amet, \nconsectetur adipiscing elit. \n\nHeading 2\n\nPellentesque hendrerit tempus massa. \nCurabitur consectetur mollis scelerisque.\n\nHeading 3\n\nUt at suscipit enim. \nSed convallis varius urna, \nquis scelerisque nisl facilisis at. \n'; - const expected4 = ''; - const actual4 = findExistingStyleCharacter(markdown4); - assert.strictEqual(actual4, expected4); - }); - - test('getSecondLevelHeading returns correct object when standard style L2 heading is passed-in', () => { - const standardLine1 = '# Top Level Heading\n'; - const standardLine2 = '## Level 2 Heading\n'; - const standardLine3 = '### Level 3 Heading\n'; - - const expectedStandard1 = {line: -1, text: '# Top Level Heading\n', isHash: true, isToc: false }; - const actualResult1 = getSecondLevelHeading(standardLine1, 0, ' ', true); - assert.strictEqual(actualResult1.line, expectedStandard1.line); - assert.strictEqual(actualResult1.text, expectedStandard1.text); - assert.strictEqual(actualResult1.isHash, expectedStandard1.isHash); - assert.strictEqual(actualResult1.isToc, expectedStandard1.isToc); - + test('getSecondLevelHeading returns correct object when standard style L2 heading is passed-in BRAVO', () => { const expectedStandard2 = { line: 4, text: 'Level 2 Heading', isHash: true, isToc: false }; const actualresult2 = getSecondLevelHeading(standardLine2, 4, '', true); - assert.strictEqual(actualresult2.line, expectedStandard2.line); - assert.strictEqual(actualresult2.text, expectedStandard2.text); - assert.strictEqual(actualresult2.isHash, expectedStandard2.isHash); - assert.strictEqual(actualresult2.isToc, expectedStandard2.isToc); + assert.strictEqual(actualresult2.line, expectedStandard2.line, lineMsg); + assert.strictEqual(actualresult2.text, expectedStandard2.text, textMsg); + assert.strictEqual(actualresult2.isHash, expectedStandard2.isHash, isHashMsg); + assert.strictEqual(actualresult2.isToc, expectedStandard2.isToc, isTocMsg); + }); + test('getSecondLevelHeading returns correct object when standard style L2 heading is passed-in CHARLIE', () => { const expectedStandard3 = { line: -1, text: '### Level 3 Heading\n', isHash: true, isToc: false }; const actualResult3 = getSecondLevelHeading(standardLine3, 8, ' ', true); - assert.strictEqual(actualResult3.line, expectedStandard3.line); - assert.strictEqual(actualResult3.text, expectedStandard3.text); - assert.strictEqual(actualResult3.isHash, expectedStandard3.isHash); - assert.strictEqual(actualResult3.isToc, expectedStandard3.isToc); + assert.strictEqual(actualResult3.line, expectedStandard3.line, lineMsg); + assert.strictEqual(actualResult3.text, expectedStandard3.text, textMsg); + assert.strictEqual(actualResult3.isHash, expectedStandard3.isHash, isHashMsg); + assert.strictEqual(actualResult3.isToc, expectedStandard3.isToc, isTocMsg); + }); + test('getSecondLevelHeading returns correct object when standard style L2 heading is passed-in DELTA', () => { const expectedStandard4 = { line: -1, text: ' ', isHash: true, isToc: false }; const actualResult4 = getSecondLevelHeading(' ', 8, standardLine2, true); - assert.strictEqual(actualResult4.line, expectedStandard4.line); - assert.strictEqual(actualResult4.text, expectedStandard4.text); - assert.strictEqual(actualResult4.isHash, expectedStandard4.isHash); - assert.strictEqual(actualResult4.isToc, expectedStandard4.isToc); + assert.strictEqual(actualResult4.line, expectedStandard4.line, lineMsg); + assert.strictEqual(actualResult4.text, expectedStandard4.text, textMsg); + assert.strictEqual(actualResult4.isHash, expectedStandard4.isHash, isHashMsg); + assert.strictEqual(actualResult4.isToc, expectedStandard4.isToc, isTocMsg); }); test('getSecondLevelHeading returns correct object when alternate style L2 heading is passed-in', () => { @@ -164,9 +59,9 @@ suite('getSecondLevelHeading Module tests', () => { const alternateLine3b = ''; const alternateLongLine = '----------------'; const alternateDashedLine = '- --'; - + // isHash is set to true by default, getSecondLevelHeading sets isHash to true even when no 2LH is found and line is set to -1 - const expectedAlt1 = { line: -1, text: 'Top Level Heading\n', isHash: true, isToc: false }; + const expectedAlt1 = { line: -1, text: 'Top Level Heading\n', isHash: false, isToc: false }; const actualResult1 = getSecondLevelHeading(alternateLine1, 0, alternateLine1b, false); assert.strictEqual(actualResult1.line, expectedAlt1.line, 'line number does not match.'); assert.strictEqual(actualResult1.text, expectedAlt1.text, 'Text does not match.'); @@ -180,7 +75,7 @@ suite('getSecondLevelHeading Module tests', () => { assert.strictEqual(actualResult2.isHash, expectedAlt2.isHash); assert.strictEqual(actualResult2.isToc, expectedAlt2.isToc); - const expectedAlt3 = { line: -1, text: 'Level 3 Heading\n', isHash: true, isToc: false }; + const expectedAlt3 = { line: -1, text: 'Level 3 Heading\n', isHash: false, isToc: false }; const actualResult3 = getSecondLevelHeading(alternateLine3, 10, alternateLine3b, false); assert.strictEqual(actualResult3.line, expectedAlt3.line); assert.strictEqual(actualResult3.text, expectedAlt3.text); @@ -202,3 +97,166 @@ suite('getSecondLevelHeading Module tests', () => { assert.strictEqual(actualResult5.isToc, expectedAlt5.isToc); }); }); + +suite('getHash2LH Function Tests', () => { + test('Standard style L2 heading line, text, and isHash true', () => { + const firstLine1 = '## Heading 1'; + const secondLine1 = ''; + const firstLineIdx1 = 2; + // the standard heading style text will always be trimmed to just the text, no hashmarks + const expected1 = { line: 2, text: 'Heading 1', isHash: true, isToc: false }; + + const actual1 = getHash2LH(firstLine1, firstLineIdx1, secondLine1); + assert.strictEqual(actual1.line, expected1.line); + assert.strictEqual(actual1.text, expected1.text); + assert.strictEqual(actual1.isHash, expected1.isHash); + assert.strictEqual(actual1.isToc, expected1.isToc); + }); + + test('Standard style L2 heading line, text, and isHash true even if second line is not empty', () => { + const firstLine2 = '## Heading 2\n'; + const secondLine2 = 'text\n'; + const firstLineIdx2 = 4; + const expected2 = { line: 4, text: 'Heading 2', isHash: true, isToc: false }; + + const actual2 = getHash2LH(firstLine2, firstLineIdx2, secondLine2); + assert.strictEqual(actual2.line, expected2.line); + assert.strictEqual(actual2.text, expected2.text); + assert.strictEqual(actual2.isHash, expected2.isHash); + assert.strictEqual(actual2.isToc, expected2.isToc); + }); + + test('Standard style L2 heading line, text, and isHash true even if firstLine has space and secondLine has newline char', () => { + const firstLine3 = '## Heading 3 \n'; + const secondLine3 = 'text\n'; + const firstLineIdx3 = 6; + const expected3 = { line: 6, text: 'Heading 3', isHash: true, isToc: false }; + + const actual3 = getHash2LH(firstLine3, firstLineIdx3, secondLine3); + assert.strictEqual(actual3.line, expected3.line); + assert.strictEqual(actual3.text, expected3.text); + assert.strictEqual(actual3.isHash, expected3.isHash); + assert.strictEqual(actual3.isToc, expected3.isToc); + }); + + test('Standard style L2 heading line, text, and isHash true even if firstLine contains string Table of Contents and secondLine is empty', () => { + const firstLine4 = '## Table of Contents'; + const secondLine4 = ' '; + const firstLineIdx4 = 4; + const expected4 = { line: 4, text: 'Table of Contents', isHash: true, isToc: false }; + + const actual4 = getHash2LH(firstLine4, firstLineIdx4, secondLine4); + assert.strictEqual(actual4.line, expected4.line, 'line number does not match.'); + assert.strictEqual(actual4.text, expected4.text, 'Text does not match.'); + assert.strictEqual(actual4.isHash, expected4.isHash, 'isHash does not match.'); + assert.strictEqual(actual4.isToc, expected4.isToc, 'isToc does not match.'); + }); + + test('Missing standard style heading characters returns correct line, text, isHash, isClosedAtx, and isToc', () => { + const firstLine5 = 'Heading 5'; + const secondLine5 = ''; + const firstLineIdx5 = 0; + const expected5 = { line: -1, text: 'Heading 5', isHash: true, isClosedAtx: false, isToc: false }; + + const actual5 = getHash2LH(firstLine5, firstLineIdx5, secondLine5); + assert.strictEqual(actual5.line, expected5.line, 'line number does not match.'); + assert.strictEqual(actual5.text, expected5.text, 'Text does not match.'); + assert.strictEqual(actual5.isHash, expected5.isHash, 'isHash does not match.'); + assert.strictEqual(actual5.isToc, expected5.isToc, 'isToc does not match.'); + }); +}); + +suite('getDash2LH Function Tests', () => { + test('Alternate style L2 heading with following line of same-number dash characters returns correct line, text, isHash, and isToc', () => { + const firstLine1 = 'Heading 1'; + const secondLine1 = '---------'; + const firstLineIdx1 = 0; + const expected1 = { line: 0, text: 'Heading 1', isHash: false, isToc: false }; + + const actual1 = getDash2LH(firstLine1, firstLineIdx1, secondLine1); + assert.strictEqual(actual1.line, expected1.line, 'line number does not match.'); + assert.strictEqual(actual1.text, expected1.text, 'Text does not match.'); + assert.strictEqual(actual1.isHash, expected1.isHash, 'isHash does not match.'); + assert.strictEqual(actual1.isToc, expected1.isToc, 'isToc does not match.'); + }); + + test('Alternate style L2 heading with single dash character returns correct line, text, isHash, and isToc', ()=>{ + const firstLine2 = 'Heading 2'; + const secondLine2 = '-'; + const firstLineIdx2 = 0; + const expected2 = { line: 0, text: 'Heading 2', isHash: false, isToc: false }; + + const actual2 = getDash2LH(firstLine2, firstLineIdx2, secondLine2); + assert.strictEqual(actual2.line, expected2.line, 'line number does not match.'); + assert.strictEqual(actual2.text, expected2.text, 'Text does not match.'); + assert.strictEqual(actual2.isHash, expected2.isHash, 'isHash does not match.'); + assert.strictEqual(actual2.isToc, expected2.isToc, 'isToc does not match.'); + }); + + test('Missing alternate style heading characters returns correct line, text, isHash, isClosedAtx, and isToc', () => { + const firstLine3 = 'Heading 3'; + const secondLine3 = ''; + const firstLineIdx3 = 0; + const expected3 = { line: -1, text: 'Heading 3', isHash: false, isClosedAtx: false, isToc: false }; + + const actual3 = getDash2LH(firstLine3, firstLineIdx3, secondLine3); + assert.strictEqual(actual3.line, expected3.line, 'line number does not match.'); + assert.strictEqual(actual3.text, expected3.text, 'Text does not match.'); + assert.strictEqual(actual3.isHash, expected3.isHash, 'isHash does not match.'); + assert.strictEqual(actual3.isToc, expected3.isToc, 'isToc does not match.'); + }); + + test('getDash2LH returns alternate style L2 heading line 2, correct Text, and second line dash character', () => { + const firstLine1 = 'Heading 1'; + const firstLineIdx1 = 2; + const secondLine1 = '-'; + // the standard heading style text will always be trimmed to just the text, no hashmarks + const expected1 = { line: 2, text: 'Heading 1', isHash: false, isClosedAtx: false, isToc: false }; + + const actual1 = getDash2LH(firstLine1, firstLineIdx1, secondLine1); + assert.strictEqual(actual1.line, expected1.line); + assert.strictEqual(actual1.text, expected1.text); + assert.strictEqual(actual1.isHash, expected1.isHash); + assert.strictEqual(actual1.isToc, expected1.isToc); + }); + + test('getDash2LH returns alternate style L2 heading line 4, correct Text, and second line dash character', () => { + const firstLine2 = 'Heading 2\n'; + const secondLine2 = '---------'; + const firstLineIdx2 = 4; + const expected2 = { line: 4, text: 'Heading 2', isHash: false, isToc: false }; + + const actual2 = getDash2LH(firstLine2, firstLineIdx2, secondLine2); + assert.strictEqual(actual2.line, expected2.line); + assert.strictEqual(actual2.text, expected2.text); + assert.strictEqual(actual2.isHash, expected2.isHash); + assert.strictEqual(actual2.isToc, expected2.isToc); + }); + + test('getDash2LH returns alternate style L2 heading line 6, correct Text, and second line dash character', () => { + const firstLine3 = 'Heading 3'; + const secondLine3 = '------- -\n'; + const firstLineIdx3 = 6; + const expected3 = { line: 6, text: 'Heading 3', isHash: false, isToc: false }; + + const actual3 = getDash2LH(firstLine3, firstLineIdx3, secondLine3); + assert.strictEqual(actual3.line, expected3.line); + assert.strictEqual(actual3.text, expected3.text); + assert.strictEqual(actual3.isHash, expected3.isHash); + assert.strictEqual(actual3.isToc, expected3.isToc); + }); + + test('getDash2LH returns alternate style L2 heading line 5, correct Text, and second line dash even if string is Table of Contents', () => { + const firstLine4 = 'Table of Contents\n'; + const secondLine4 = '-----------------\n'; + const firstLineIdx4 = 5; + const expected4 = { line: 5, text: 'Table of Contents', isHash: false, isClosedAtx: false, isToc: false }; + + const actual4 = getDash2LH(firstLine4, firstLineIdx4, secondLine4); + assert.strictEqual(actual4.line, expected4.line); + assert.strictEqual(actual4.text, expected4.text); + assert.strictEqual(actual4.isHash, expected4.isHash); + assert.strictEqual(actual4.isToc, expected4.isToc); + }); + +}); diff --git a/test/suite/processHeadings.test.js b/test/suite/processHeadings.test.js new file mode 100644 index 0000000..4d43e3f --- /dev/null +++ b/test/suite/processHeadings.test.js @@ -0,0 +1,104 @@ +const assert = require('assert'); +const { + getTitleOnly, + getLoweredKebabCase, + getLinkFragment +} = require('../../extension-functions/process-headings'); + +suite('process-headings module tests', () => { + // global variables: input headings + const firstOpenAtxHeading = '## First 2H Heading'; + const firstClosedAtxHeading = '## First 2H Heading ##'; + + const secondOpenAtxHeading = '## Second 2H Heading'; + const secondClosedAtxHeading = '## Second 2H Heading ##'; + + const thirdOpenAtxHeading = '## Third 2H Heading '; + const thirdClosedAtxHeading = '## Third 2H Heading ##'; + + const dotNetOpenAtxHeading = '## Dotnet.net'; + const dotNetClosedAtxHeading = '## Dotnet.net ##'; + + const itemsToDosOpenAtxHeading = '## Items / Todos'; + const itemsToDosClosedAtxHeading = '## Items / Todos ##'; + + const aspnetCoreOpenAtxHeading = '## Aspnetcore, Blazor'; + const aspnetCoreClosedAtxHeading = '## Aspnetcore, Blazor ##'; + + const underscoreLowerCaseCharOpenAtxHeading = '## Underscore_lowercase Character'; + const underscoreLowerCaseCharClosedAtxHeading = '## Underscore_lowercase Character ##'; + + const illegalCharactersOpenAtxHeading = '## Illegal _ - < > Characters'; + const illegalCharactersClosedAtxHeading = '## Illegal _ - < > Characters ##'; + + // global variables: expected heading texts without heading characters or illegal characters + const expectedFirstHeading = 'First 2H Heading'; + const expectedSecondHeading = 'Second 2H Heading'; + const expectedThirdHeading = 'Third 2H Heading '; + const expectedDotNetHeading = 'Dotnet.net'; + const expectedItemsTodosHeading = 'Items / Todos'; + const expectedAspnetCoreHeading = 'Aspnetcore, Blazor'; + const expectedUnderscoreLowerCaseChar = 'Underscore_lowercase Character'; + const expectedIllegalCharactersHeading = 'Illegal _ - Characters'; + + // global variables: expected lowered-kebab-case headings + const expectedFirstLoweredKebabHeading = 'first-2h-heading'; + const expectedSecondLoweredKebabHeading = 'second--2h--heading'; + const expectedThirdLoweredKebabHeading = 'third---2h---heading'; + const expectedDotnetNetLoweredKebabHeading = 'dotnetnet'; + const expectedItemsTodosLoweredKebabHeading = 'items--todos'; + const expectedAspnetCoreLoweredKebabHeading = 'aspnetcore-blazor'; + const expectedUnderscoreLoweredKebabCaseChar = 'underscore_lowercase-character'; + const expectedIllegalCharactersLoweredKebab = 'illegal-_-----characters'; + + // global variables: expected table of content entries + const expectedFirstTocResult = '- [First 2H Heading](#first-2h-heading)\n'; + const expectedSecondTocResult = '- [Second 2H Heading](#second--2h--heading)\n'; + const expectedThirdTocResult = '- [Third 2H Heading ](#third---2h---heading)\n'; + const expectedDotnetNetTocResult = '- [Dotnet.net](#dotnetnet)\n'; + const expectedItemsTodosTocResult = '- [Items / Todos](#items--todos)\n'; + const expectedAspnetCoreTocResult = '- [Aspnetcore, Blazor](#aspnetcore-blazor)\n'; + const expectedUnderscoreLowerCharTocResult = '- [Underscore_lowercase Character](#underscore_lowercase-character)\n'; + const expectedIllegalCharsTocResult = '- [Illegal Characters](#illegal-_-----characters)\n'; + + test('Get title only from a string of text', () => { + assert.strictEqual(getTitleOnly(firstOpenAtxHeading), firstOpenAtxHeading, 'firstOpenAtxHeading title does not match.'); + assert.strictEqual(getTitleOnly(firstClosedAtxHeading), firstClosedAtxHeading, 'firstClosedAtxHeading title does not match.'); + assert.strictEqual(getTitleOnly(secondOpenAtxHeading), secondOpenAtxHeading, 'secondOpenAtxHeading title does not match.'); + assert.strictEqual(getTitleOnly(secondClosedAtxHeading), secondClosedAtxHeading, 'secondClosedAtxHeading title does not match.'); + assert.strictEqual(getTitleOnly(thirdOpenAtxHeading), thirdOpenAtxHeading.trim(), 'thirdOpenAtxHeading title does not match.'); + assert.strictEqual(getTitleOnly(thirdClosedAtxHeading), thirdClosedAtxHeading, 'thirdClosedAtxHeading title does not match.'); + assert.strictEqual(getTitleOnly(dotNetOpenAtxHeading), '## ' + expectedDotNetHeading, 'dotNetOpenAtxHeading with leading ## does not match.'); + assert.strictEqual(getTitleOnly(dotNetClosedAtxHeading), '## ' + expectedDotNetHeading + ' ##', 'dotNetClosedAtxHeading with leading and trailing ## does not match.'); + assert.strictEqual(getTitleOnly(itemsToDosOpenAtxHeading), '## ' + expectedItemsTodosHeading, 'The title should be ## Items / Todos'); + assert.strictEqual(getTitleOnly(itemsToDosClosedAtxHeading), '## ' + expectedItemsTodosHeading + ' ##', 'The title should be ## Items / Todos ##'); + assert.strictEqual(getTitleOnly(aspnetCoreOpenAtxHeading), '## ' + expectedAspnetCoreHeading, 'The title should be ## Aspnetcore, Blazor'); + assert.strictEqual(getTitleOnly(aspnetCoreClosedAtxHeading), '## ' + expectedAspnetCoreHeading + ' ##', 'The title should be ## Aspnetcore, Blazor ##'); + assert.strictEqual(getTitleOnly(underscoreLowerCaseCharOpenAtxHeading), '## ' + expectedUnderscoreLowerCaseChar, 'The title should be ## Underscore_lowercase Character'); + assert.strictEqual(getTitleOnly(underscoreLowerCaseCharClosedAtxHeading), '## ' + expectedUnderscoreLowerCaseChar + ' ##', 'The title should be ## Underscore_lowercase Character ##'); + assert.strictEqual(getTitleOnly(illegalCharactersOpenAtxHeading), '## ' + expectedIllegalCharactersHeading, 'The title should be ## Illegal Characters'); + assert.strictEqual(getTitleOnly(illegalCharactersClosedAtxHeading), '## ' + expectedIllegalCharactersHeading + ' ##', 'The title should be ## Illegal Characters ##'); + }); + + test('Get lower-kebab-case version of input text', () => { + assert.strictEqual(getLoweredKebabCase(expectedFirstHeading), expectedFirstLoweredKebabHeading); + assert.strictEqual(getLoweredKebabCase(expectedSecondHeading), expectedSecondLoweredKebabHeading); + assert.strictEqual(getLoweredKebabCase(expectedThirdHeading), expectedThirdLoweredKebabHeading); + assert.strictEqual(getLoweredKebabCase(expectedDotNetHeading), expectedDotnetNetLoweredKebabHeading); + assert.strictEqual(getLoweredKebabCase(expectedItemsTodosHeading), expectedItemsTodosLoweredKebabHeading); + assert.strictEqual(getLoweredKebabCase(expectedAspnetCoreHeading), expectedAspnetCoreLoweredKebabHeading); + assert.strictEqual(getLoweredKebabCase(expectedUnderscoreLowerCaseChar), expectedUnderscoreLoweredKebabCaseChar); + assert.strictEqual(getLoweredKebabCase(expectedIllegalCharactersHeading), expectedIllegalCharactersLoweredKebab); + }); + + test('Get valid link fragment from a title string and a lowered-kebab-case string', () => { + assert.strictEqual(getLinkFragment(expectedFirstHeading, expectedFirstLoweredKebabHeading), expectedFirstTocResult); + assert.strictEqual(getLinkFragment(expectedSecondHeading, expectedSecondLoweredKebabHeading), expectedSecondTocResult); + assert.strictEqual(getLinkFragment(expectedThirdHeading, expectedThirdLoweredKebabHeading), expectedThirdTocResult, 'Third ToC Fragment does not match (missing the trailing spaces?)'); + assert.strictEqual(getLinkFragment(expectedDotNetHeading, expectedDotnetNetLoweredKebabHeading), expectedDotnetNetTocResult); + assert.strictEqual(getLinkFragment(expectedItemsTodosHeading, expectedItemsTodosLoweredKebabHeading), expectedItemsTodosTocResult); + assert.strictEqual(getLinkFragment(expectedAspnetCoreHeading, expectedAspnetCoreLoweredKebabHeading), expectedAspnetCoreTocResult); + assert.strictEqual(getLinkFragment(expectedUnderscoreLowerCaseChar, expectedUnderscoreLoweredKebabCaseChar), expectedUnderscoreLowerCharTocResult); + assert.notStrictEqual(getLinkFragment(expectedIllegalCharactersHeading, expectedIllegalCharactersLoweredKebab), expectedIllegalCharsTocResult, 'Illegal characters are stripped from the title and/or link fragment but should not be.'); + }); +});