Flet-github-slugger-3461c43/000077500000000000000000000000001432653302400156055ustar00rootroot00000000000000Flet-github-slugger-3461c43/.github/000077500000000000000000000000001432653302400171455ustar00rootroot00000000000000Flet-github-slugger-3461c43/.github/workflows/000077500000000000000000000000001432653302400212025ustar00rootroot00000000000000Flet-github-slugger-3461c43/.github/workflows/main.yml000066400000000000000000000006471432653302400226600ustar00rootroot00000000000000name: main on: - pull_request - push jobs: main: name: ${{matrix.node}} runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: dcodeIO/setup-node-nvm@master with: node-version: ${{matrix.node}} - run: npm install - run: npm test - uses: codecov/codecov-action@v1 strategy: matrix: node: - lts/fermium - node Flet-github-slugger-3461c43/.gitignore000066400000000000000000000011411432653302400175720ustar00rootroot00000000000000# Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Coverage directory used by tools like nyc .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules package-lock.json *.d.ts Flet-github-slugger-3461c43/.npmrc000066400000000000000000000000231432653302400167200ustar00rootroot00000000000000package-lock=false Flet-github-slugger-3461c43/CHANGELOG.md000066400000000000000000000032671432653302400174260ustar00rootroot00000000000000# github-slugger change log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## 2.0.0 2022-10-27 * Use ESM **breaking**: please read [this guide](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c) * Add types **breaking**: tiny chance of breaking, use a new version of TS and it’ll work ## 1.5.0 2022-10-25 * Update Unicode to 13, to match GH ## 1.4.0 2021-08-24 * Fix to match GitHub’s algorithm on unicode ## 1.3.0 2020-02-21 * Expose static slug function for folks who do not want/need the stateful bits (Thanks [@bobbylito](https://github.com/bobylito)!). ## 1.2.1 2019-xx-xx * Fix collisions for slugs with occurrences * Fix accessing `Object.prototype` methods * Fix Russian * Add `files` to package.json ## 1.2.0 2017-09-21 * Add `maintainCase` support ## 1.1.3 2017-05-29 * Fix`emoji-regex` semver version to ensure npm5 compatibility. ## 1.1.2 2017-05-26 * Lock down `emoji-regex` dependency to avoid [strange unicode bug](https://github.com/Flet/github-slugger/issues/9) ## 1.1.1 * Add more conformant unicode handling to ensure: - emoji are correctly stripped - non-Latin characters are not incorrectly lowercased * Also adds more verified test cases Check the [PR](https://github.com/Flet/github-slugger/pull/8) for more details! Thanks [@wooorm](https://github.com/wooorm)! ## 1.1.0 * Feature: Support for non-latin characters in slugs https://github.com/Flet/github-slugger/pull/3) Thanks [@tadatuta](https://github.com/tadatuta)! ## 1.0.1 * Fix: bug for multiple empty slugds (https://github.com/Flet/github-slugger/pull/1) Thanks [@wooorm](https://github.com/wooorm)! Flet-github-slugger-3461c43/CONTRIBUTING.md000066400000000000000000000052421432653302400200410ustar00rootroot00000000000000# Contributing Guidelines Contributions welcome! **Before spending lots of time on something, ask for feedback on your idea first!** Please search issues and pull requests before adding something new to avoid duplicating efforts and conversations. In addition to improving the project by refactoring code and implementing relevant features, this project welcomes the following types of contributions: - **Ideas**: participate in an issue thread or start your own to have your voice heard. - **Writing**: contribute your expertise in an area by helping expand the included content. - **Copy editing**: fix typos, clarify language, and generally improve the quality of the content. - **Formatting**: help keep content easy to read with consistent formatting. ## Installing Fork and clone the repo, then `npm install` to install all dependencies. ## Testing Tests are run with `npm test`. Unless you're creating a failing test to increase test coverage or show a problem, please make sure all tests are passing before submitting a pull request. ## Code Style [![standard][standard-image]][standard-url] This repository uses [`standard`][standard-url] to maintain code style and consistency and avoid style arguments. `npm test` runs `standard` so you don't have to! [standard-image]: https://cdn.rawgit.com/feross/standard/master/badge.svg [standard-url]: https://github.com/feross/standard [semistandard-image]: https://cdn.rawgit.com/flet/semistandard/master/badge.svg [semistandard-url]: https://github.com/Flet/semistandard --- # Collaborating Guidelines **This is an OPEN Open Source Project.** ## What? Individuals making significant and valuable contributions are given commit access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. ## Rules There are a few basic ground rules for collaborators: 1. **No `--force` pushes** or modifying the Git history in any way. 1. **Non-master branches** ought to be used for ongoing work. 1. **External API changes and significant modifications** ought to be subject to an **internal pull request** to solicit feedback from other collaborators. 1. Internal pull requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. 1. Contributors should attempt to adhere to the prevailing code style. ## Releases Declaring formal releases remains the prerogative of the project maintainer. ## Changes to this arrangement This is an experiment and feedback is welcome! This document may also be subject to pull requests or changes by collaborators where you believe you have something valuable to add or change. Flet-github-slugger-3461c43/LICENSE000066400000000000000000000013561432653302400166170ustar00rootroot00000000000000Copyright (c) 2015, Dan Flettre Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. Flet-github-slugger-3461c43/README.md000066400000000000000000000034701432653302400170700ustar00rootroot00000000000000# github-slugger [![npm][npm-image]][npm-url] [![Build][build-badge]][build] [npm-image]: https://img.shields.io/npm/v/github-slugger.svg?style=flat-square [npm-url]: https://www.npmjs.com/package/github-slugger [build-badge]: https://github.com/Flet/github-slugger/workflows/main/badge.svg [build]: https://github.com/Flet/github-slugger/actions Generate a slug just like GitHub does for markdown headings. It also ensures slugs are unique in the same way GitHub does it. The overall goal of this package is to emulate the way GitHub handles generating markdown heading anchors as close as possible. This project is not a markdown or HTML parser: passing `alpha *bravo* charlie` or `alpha bravo charlie` doesn’t work. Instead pass the plain text value of the heading: `alpha bravo charlie`. ## Install ``` npm install github-slugger ``` ## Usage ```js import GithubSlugger from 'github-slugger' const slugger = new GithubSlugger() slugger.slug('foo') // returns 'foo' slugger.slug('foo') // returns 'foo-1' slugger.slug('bar') // returns 'bar' slugger.slug('foo') // returns 'foo-2' slugger.slug('Привет non-latin 你好') // returns 'привет-non-latin-你好' slugger.slug('😄 emoji') // returns '-emoji' slugger.reset() slugger.slug('foo') // returns 'foo' ``` Check [`test/fixtures.json`](test/fixtures.json) for more examples. If you need, you can also use the underlying implementation which does not keep track of the previously slugged strings (not recommended): ```js import GithubSlugger, {slug} from 'github-slugger' slug('foo bar baz') // returns 'foo-bar-baz' slug('foo bar baz') // returns the same slug 'foo-bar-baz' because it does not keep track ``` ## Contributing Contributions welcome! Please read the [contributing guidelines](CONTRIBUTING.md) first. ## License [ISC](LICENSE) Flet-github-slugger-3461c43/index.js000066400000000000000000000034631432653302400172600ustar00rootroot00000000000000import { regex } from './regex.js' const own = Object.hasOwnProperty /** * Slugger. */ export default class BananaSlug { /** * Create a new slug class. */ constructor () { /** @type {Record} */ // eslint-disable-next-line no-unused-expressions this.occurrences this.reset() } /** * Generate a unique slug. * * Tracks previously generated slugs: repeated calls with the same value * will result in different slugs. * Use the `slug` function to get same slugs. * * @param {string} value * String of text to slugify * @param {boolean} [maintainCase=false] * Keep the current case, otherwise make all lowercase * @return {string} * A unique slug string */ slug (value, maintainCase) { const self = this let result = slug(value, maintainCase === true) const originalSlug = result while (own.call(self.occurrences, result)) { self.occurrences[originalSlug]++ result = originalSlug + '-' + self.occurrences[originalSlug] } self.occurrences[result] = 0 return result } /** * Reset - Forget all previous slugs * * @return void */ reset () { this.occurrences = Object.create(null) } } /** * Generate a slug. * * Does not track previously generated slugs: repeated calls with the same value * will result in the exact same slug. * Use the `GithubSlugger` class to get unique slugs. * * @param {string} value * String of text to slugify * @param {boolean} [maintainCase=false] * Keep the current case, otherwise make all lowercase * @return {string} * A unique slug string */ export function slug (value, maintainCase) { if (typeof value !== 'string') return '' if (!maintainCase) value = value.toLowerCase() return value.replace(regex, '').replace(/ /g, '-') } Flet-github-slugger-3461c43/package.json000066400000000000000000000035031432653302400200740ustar00rootroot00000000000000{ "name": "github-slugger", "description": "Generate a slug just like GitHub does for markdown headings.", "version": "2.0.0", "author": "Dan Flettre ", "contributors": [ "Dan Flettre ", "Titus Wormer (http://wooorm.com)" ], "bugs": { "url": "https://github.com/Flet/github-slugger/issues" }, "type": "module", "main": "index.js", "types": "index.d.ts", "files": [ "index.d.ts", "index.js", "regex.d.ts", "regex.js" ], "devDependencies": { "@octokit/rest": "^19.0.0", "@types/regenerate": "^1.0.0", "@types/tape": "^4.0.0", "@unicode/unicode-13.0.0": "^1.0.0", "c8": "^7.0.0", "hast-util-select": "^5.0.0", "mdast-util-gfm": "^2.0.0", "mdast-util-to-markdown": "^1.0.0", "node-fetch": "^3.0.0", "regenerate": "^1.0.0", "rehype-parse": "^8.0.0", "rimraf": "^3.0.0", "standard": "*", "tap-spec": "^5.0.0", "tape": "^5.0.0", "type-coverage": "^2.0.0", "typescript": "^4.0.0", "unified": "^10.0.0" }, "homepage": "https://github.com/Flet/github-slugger", "keywords": [ "anchor", "github", "hash", "heading", "markdown", "slug", "slugger", "url" ], "license": "ISC", "repository": { "type": "git", "url": "https://github.com/Flet/github-slugger.git" }, "scripts": { "prepack": "npm run build && npm run format", "build": "rimraf \"{script,test}/**/*.d.ts\" \"*.d.ts\" && tsc && type-coverage", "format": "standard --fix", "test-api": "tape test | tap-spec", "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", "test": "npm run build && npm run format && npm run test-coverage" }, "typeCoverage": { "atLeast": 100, "detail": true, "strict": true } } Flet-github-slugger-3461c43/script/000077500000000000000000000000001432653302400171115ustar00rootroot00000000000000Flet-github-slugger-3461c43/script/generate-fixtures.js000066400000000000000000000140161432653302400231120ustar00rootroot00000000000000import { promises as fs } from 'node:fs' import { Octokit } from '@octokit/rest' import fetch from 'node-fetch' import { unified } from 'unified' import rehypeParse from 'rehype-parse' import { select, selectAll } from 'hast-util-select' import { toMarkdown } from 'mdast-util-to-markdown' import { gfmToMarkdown } from 'mdast-util-gfm' // Note: the GH token needs `gists` access! const ghToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN if (!ghToken) { throw new Error('Missing GitHub token: expected `GH_TOKEN` in env') } const octo = new Octokit({ auth: 'token ' + ghToken }) const categoryBase = new URL('../node_modules/@unicode/unicode-13.0.0/General_Category/', import.meta.url) // Take up to N samples from each category. const samples = 400 const otherTests = [ { name: 'Basic usage', input: 'alpha' }, { name: 'Basic usage (again)', input: 'alpha' }, { name: 'Camelcase', input: 'bravoCharlieDelta' }, { name: 'Prototypal injection: proto', input: '__proto__' }, { name: 'Prototypal injection: proto (again)', input: '__proto__' }, { name: 'Prototypal injection: has own', input: 'hasOwnProperty' }, { name: 'Repetition (1)', input: 'echo' }, { name: 'Repetition (2)', input: 'echo' }, { name: 'Repetition (3)', input: 'echo 1' }, { name: 'Repetition (4)', input: 'echo-1' }, { name: 'Repetition (5)', input: 'echo' }, { name: 'More repetition (1)', input: 'foxtrot-1' }, { name: 'More repetition (2)', input: 'foxtrot' }, { name: 'More repetition (3)', input: 'foxtrot' }, { name: 'Characters: dash', input: 'heading with a - dash' }, { name: 'Characters: underscore', input: 'heading with an _ underscore' }, { name: 'Characters: dot', input: 'heading with a period.txt' }, { name: 'Characters: dots, parents, brackets', input: 'exchange.bind_headers(exchange, routing [, bindCallback])' }, { name: 'Characters: space', input: ' ', markdownOverwrite: '# ' }, { name: 'Characters: initial space', input: ' a', markdownOverwrite: '# a' }, { name: 'Characters: final space', input: 'a ', markdownOverwrite: '# a ' }, { name: 'Characters: initial and final spaces', input: ' a ', markdownOverwrite: '# a ' }, { name: 'Characters: initial and final dashes', input: '-a-' }, { name: 'Characters: apostrophe', input: 'apostrophe’s should be trimmed' }, { name: 'Some more duplicates (1)', input: 'golf' }, { name: 'Some more duplicates (2)', input: 'golf' }, { name: 'Some more duplicates (3)', input: 'golf' }, { name: 'Non-ascii: ♥', input: 'I ♥ unicode' }, { name: 'Non-ascii: -', input: 'dash-dash' }, { name: 'Non-ascii: –', input: 'en–dash' }, { name: 'Non-ascii: –', input: 'em–dash' }, { name: 'Non-ascii: 😄', input: '😄 unicode emoji' }, { name: 'Non-ascii: 😄-😄', input: '😄-😄 unicode emoji' }, { name: 'Non-ascii: 😄_😄', input: '😄_😄 unicode emoji' }, { name: 'Non-ascii: 😄', input: '😄 - an emoji' }, { name: 'Non-ascii: :smile:', input: ':smile: - a gemoji' }, { name: 'Non-ascii: Cyrillic (1)', input: 'Привет' }, { name: 'Non-ascii: Cyrillic (2)', input: 'Профили пользователей' }, { name: 'Non-ascii: Cyrillic + Han', input: 'Привет non-latin 你好' }, { name: 'Gemoji (1)', input: ':ok: No underscore' }, { name: 'Gemoji (2)', input: ':ok_hand: Single' }, { name: 'Gemoji (3)', input: ':ok_hand::hatched_chick: Two in a row with no spaces' }, { name: 'Gemoji (4)', input: ':ok_hand: :hatched_chick: Two in a row' } ] const files = await fs.readdir(categoryBase) /** @type {Array<{name: string, input: string, markdownOverwrite?: string, expected?: string}>} */ const tests = [...otherTests] let index = -1 // Create a test case with a bunch of examples. while (++index < files.length) { const name = files[index] if (name === 'index.js') continue // These result in Git(Hub) thinking it’s a binary file. if (name === 'Control' || name === 'Surrogate') continue // This prevents GH from rendering markdown to HTML. if (name === 'Other') continue /** @type {{default: Array}} */ const { default: codePoints } = await import(new URL(name + '/code-points.js', categoryBase).href) /** @type {Array} */ const subs = [] let n = -1 while (++n < samples) { subs.push(codePoints[Math.floor(codePoints.length / samples * n)]) } subs.push(codePoints[codePoints.length - 1]) tests.push({ name, input: 'a' + [...new Set(subs)].map(d => String.fromCodePoint(d)).join(' ') + 'b' }) } // Create a Gist. const filename = 'readme.md' const gistResult = await octo.gists.create({ files: { [filename]: { content: tests.map(d => { return d.markdownOverwrite || toMarkdown({ type: 'heading', depth: 1, children: [{ type: 'text', value: d.input }] }, { extensions: [gfmToMarkdown()] }) }).join('\n\n') } } }) const file = (gistResult.data.files || {})[filename] if (!file || !gistResult.data.html_url || !gistResult.data.id) { throw new Error('Something weird happened contacting GitHub') } if (!file.language) { throw new Error('The generated markdown was seen as binary data instead of text by GitHub. This is likely because there are weird characters (such as control characters or lone surrogates) in it') } // Fetch the rendered page. const response = await fetch(gistResult.data.html_url, { headers: { Authorization: 'token ' + ghToken } }) const doc = await response.text() // Remove the Gist. await octo.gists.delete({ gist_id: gistResult.data.id }) const tree = unified().use(rehypeParse).parse(doc) const markdownBody = select('.markdown-body', tree) if (!markdownBody) { throw new Error('The generated markdown could not be rendered by GitHub as HTML. This is likely because there are weird characters in it') } const anchors = selectAll('h1 .anchor', markdownBody) anchors.forEach((node, i) => { const href = (node.properties || {}).href if (typeof href === 'string') { tests[i].expected = href.slice(1) } }) await fs.writeFile(new URL('../test/fixtures.json', import.meta.url), JSON.stringify(tests, null, 2) + '\n') Flet-github-slugger-3461c43/script/generate-regex.js000066400000000000000000000026701432653302400223560ustar00rootroot00000000000000import { promises as fs } from 'node:fs' import regenerate from 'regenerate' // @ts-expect-error: untyped import alphabetics from '@unicode/unicode-13.0.0/Binary_Property/Alphabetic/code-points.js' const categoryBase = new URL('../node_modules/@unicode/unicode-13.0.0/General_Category/', import.meta.url) // Unicode General Categories to remove. const ranges = [ // Some numbers: 'Other_Number', // Some punctuation: 'Close_Punctuation', 'Final_Punctuation', 'Initial_Punctuation', 'Open_Punctuation', 'Other_Punctuation', // All except a normal `-` (dash) 'Dash_Punctuation', // All: 'Symbol', 'Control', 'Private_Use', 'Format', 'Unassigned', // All except a normal ` ` (space) 'Separator' ] const generator = regenerate() let index = -1 // Add code points to strip. while (++index < ranges.length) { const name = ranges[index] /** @type {{default: Array}} */ const { default: codePoints } = await import(new URL(name + '/code-points.js', categoryBase).href) generator.add(codePoints) } generator // Some overlap between letters and Other Symbol. .remove(alphabetics) // Spaces are turned to `-` .remove(' ') // Dash is kept. .remove('-') await fs.writeFile('regex.js', [ '// This module is generated by `script/`.', '/* eslint-disable no-control-regex, no-misleading-character-class, no-useless-escape */', 'export const regex = ' + generator.toRegExp() + 'g', '' ].join('\n')) Flet-github-slugger-3461c43/test/000077500000000000000000000000001432653302400165645ustar00rootroot00000000000000Flet-github-slugger-3461c43/test/index.js000066400000000000000000000025271432653302400202370ustar00rootroot00000000000000import fs from 'node:fs' import test from 'tape' import GithubSlugger, { slug } from '../index.js' /** @type {Array<{name: string, input: string, markdownOverwrite?: string, expected: string}>} */ const fixtures = JSON.parse( String(fs.readFileSync( new URL('fixtures.json', import.meta.url) )) ) test('basic', function (t) { const slugger = new GithubSlugger() // @ts-expect-error: not allowed by types but handled gracefully in the code. t.equals(slugger.slug(1), '', 'should return empty string for non-strings') // Note: GH doesn’t support `maintaincase`, so the actual values are commented below. t.equals(slugger.slug('fooCamelCase', true), 'fooCamelCase', 'should support `maintainCase`') // foocamelcase t.equals(slugger.slug('fooCamelCase'), 'foocamelcase', 'should support `maintainCase` (reference)') // foocamelcase-1 t.equals(slugger.slug('asd'), 'asd', 'should slug') t.equals(slugger.slug('asd'), 'asd-1', 'should create unique slugs for repeated values') t.end() }) test('static method', function (t) { t.equals(slug('foo'), 'foo', 'should slug') t.equals(slug('foo'), 'foo', 'should create same slugs for repeated values') t.end() }) test('fixtures', function (t) { const slugger = new GithubSlugger() fixtures.forEach((d) => { t.equals(slugger.slug(d.input), d.expected, d.name) }) t.end() }) Flet-github-slugger-3461c43/tsconfig.json000066400000000000000000000005721432653302400203200ustar00rootroot00000000000000{ "include": ["script/**/*.js", "test/**/*.js", "*.js"], "compilerOptions": { "target": "es2022", "lib": ["es2022"], "module": "esnext", "moduleResolution": "node", "allowJs": true, "checkJs": true, "declaration": true, "emitDeclarationOnly": true, "allowSyntheticDefaultImports": true, "skipLibCheck": true, "strict": true } }