planning
All checks were successful
Publish To Prod / deploy_and_publish (push) Successful in 35s

This commit is contained in:
2024-10-14 09:15:30 +02:00
parent bcba00a730
commit 6e64e138e2
21059 changed files with 2317811 additions and 1 deletions

4
node_modules/mdast-util-math/complex-types.d.ts generated vendored Normal file
View File

@@ -0,0 +1,4 @@
// To do: next major: remove this file.
export type {Math, InlineMath} from './index.js'
/// <reference types="./index.js" />

88
node_modules/mdast-util-math/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,88 @@
import type {Literal} from 'mdast'
export {mathFromMarkdown, mathToMarkdown} from './lib/index.js'
export type {ToOptions} from './lib/index.js'
/**
* Math (flow).
*/
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export interface Math extends Literal {
/**
* Node type.
*/
type: 'math'
/**
* Custom information relating to the node.
*/
meta?: string | undefined | null
}
/**
* Math (text).
*/
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export interface InlineMath extends Literal {
/**
* Node type.
*/
type: 'inlineMath'
}
// Add custom data tracked to turn markdown into a tree.
declare module 'mdast-util-from-markdown' {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface CompileData {
/**
* Whether were in math (flow).
*/
mathFlowInside?: boolean | undefined
}
}
// Add custom data tracked to turn a tree into markdown.
declare module 'mdast-util-to-markdown' {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface ConstructNameMap {
/**
* Math (flow).
*
* ```markdown
* > | $$
* ^^
* > | a
* ^
* > | $$
* ^^
* ```
*/
mathFlow: 'mathFlow'
/**
* Math (flow) meta flag.
*
* ```markdown
* > | $$a
* ^
* | b
* | $$
* ```
*/
mathFlowMeta: 'mathFlowMeta'
}
}
// Add nodes to tree.
declare module 'mdast' {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface StaticPhrasingContentMap {
inlineMath: InlineMath
}
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface BlockContentMap {
math: Math
}
}

2
node_modules/mdast-util-math/index.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
// Note: Types exported from `index.d.ts`.
export {mathFromMarkdown, mathToMarkdown} from './lib/index.js'

38
node_modules/mdast-util-math/lib/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,38 @@
/**
* Create an extension for `mdast-util-from-markdown`.
*
* @returns {FromMarkdownExtension}
* Extension for `mdast-util-from-markdown`.
*/
export function mathFromMarkdown(): FromMarkdownExtension
/**
* Create an extension for `mdast-util-to-markdown`.
*
* @param {ToOptions | null | undefined} [options]
* Configuration.
* @returns {ToMarkdownExtension}
* Extension for `mdast-util-to-markdown`.
*/
export function mathToMarkdown(
options?: ToOptions | null | undefined
): ToMarkdownExtension
export type CompileContext = import('mdast-util-from-markdown').CompileContext
export type FromMarkdownExtension = import('mdast-util-from-markdown').Extension
export type FromMarkdownHandle = import('mdast-util-from-markdown').Handle
export type ToMarkdownExtension = import('mdast-util-to-markdown').Options
export type ToMarkdownHandle = import('mdast-util-to-markdown').Handle
export type Math = import('../index.js').Math
export type InlineMath = import('../index.js').InlineMath
/**
* Configuration.
*/
export type ToOptions = {
/**
* Whether to support math (text) with a single dollar.
*
* Single dollars work in Pandoc and many other places, but often interfere
* with “normal” dollars in text.
* If you turn this off, you can still use two or more dollars for text math.
*/
singleDollarTextMath?: boolean | null | undefined
}

306
node_modules/mdast-util-math/lib/index.js generated vendored Normal file
View File

@@ -0,0 +1,306 @@
/**
* @typedef {import('mdast-util-from-markdown').CompileContext} CompileContext
* @typedef {import('mdast-util-from-markdown').Extension} FromMarkdownExtension
* @typedef {import('mdast-util-from-markdown').Handle} FromMarkdownHandle
* @typedef {import('mdast-util-to-markdown').Options} ToMarkdownExtension
* @typedef {import('mdast-util-to-markdown').Handle} ToMarkdownHandle
* @typedef {import('../index.js').Math} Math
* @typedef {import('../index.js').InlineMath} InlineMath
*
* @typedef ToOptions
* Configuration.
* @property {boolean | null | undefined} [singleDollarTextMath=true]
* Whether to support math (text) with a single dollar.
*
* Single dollars work in Pandoc and many other places, but often interfere
* with “normal” dollars in text.
* If you turn this off, you can still use two or more dollars for text math.
*/
import {longestStreak} from 'longest-streak'
import {safe} from 'mdast-util-to-markdown/lib/util/safe.js'
import {track} from 'mdast-util-to-markdown/lib/util/track.js'
import {patternCompile} from 'mdast-util-to-markdown/lib/util/pattern-compile.js'
/**
* Create an extension for `mdast-util-from-markdown`.
*
* @returns {FromMarkdownExtension}
* Extension for `mdast-util-from-markdown`.
*/
export function mathFromMarkdown() {
return {
enter: {
mathFlow: enterMathFlow,
mathFlowFenceMeta: enterMathFlowMeta,
mathText: enterMathText
},
exit: {
mathFlow: exitMathFlow,
mathFlowFence: exitMathFlowFence,
mathFlowFenceMeta: exitMathFlowMeta,
mathFlowValue: exitMathData,
mathText: exitMathText,
mathTextData: exitMathData
}
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function enterMathFlow(token) {
this.enter(
{
type: 'math',
meta: null,
value: '',
data: {
hName: 'div',
hProperties: {className: ['math', 'math-display']},
hChildren: [{type: 'text', value: ''}]
}
},
token
)
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function enterMathFlowMeta() {
this.buffer()
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMathFlowMeta() {
const data = this.resume()
const node = /** @type {Math} */ (this.stack[this.stack.length - 1])
node.meta = data
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMathFlowFence() {
// Exit if this is the closing fence.
if (this.getData('mathFlowInside')) return
this.buffer()
this.setData('mathFlowInside', true)
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMathFlow(token) {
const data = this.resume().replace(/^(\r?\n|\r)|(\r?\n|\r)$/g, '')
const node = /** @type {Math} */ (this.exit(token))
node.value = data
// @ts-expect-error: we defined it.
node.data.hChildren[0].value = data
this.setData('mathFlowInside')
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function enterMathText(token) {
this.enter(
{
type: 'inlineMath',
value: '',
data: {
hName: 'span',
hProperties: {className: ['math', 'math-inline']},
hChildren: [{type: 'text', value: ''}]
}
},
token
)
this.buffer()
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMathText(token) {
const data = this.resume()
const node = /** @type {Math} */ (this.exit(token))
node.value = data
// @ts-expect-error: we defined it.
node.data.hChildren[0].value = data
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMathData(token) {
this.config.enter.data.call(this, token)
this.config.exit.data.call(this, token)
}
}
/**
* Create an extension for `mdast-util-to-markdown`.
*
* @param {ToOptions | null | undefined} [options]
* Configuration.
* @returns {ToMarkdownExtension}
* Extension for `mdast-util-to-markdown`.
*/
export function mathToMarkdown(options) {
let single = (options || {}).singleDollarTextMath
if (single === null || single === undefined) {
single = true
}
inlineMath.peek = inlineMathPeek
return {
unsafe: [
{character: '\r', inConstruct: 'mathFlowMeta'},
{character: '\n', inConstruct: 'mathFlowMeta'},
{
character: '$',
after: single ? undefined : '\\$',
inConstruct: 'phrasing'
},
{character: '$', inConstruct: 'mathFlowMeta'},
{atBreak: true, character: '$', after: '\\$'}
],
handlers: {math, inlineMath}
}
/**
* @type {ToMarkdownHandle}
* @param {Math} node
*/
// To do: next major: rename `context` to state, `safeOptions` to info.
// Note: fixing this code? Please also fix the similar code for code:
// <https://github.com/syntax-tree/mdast-util-to-markdown/blob/main/lib/handle/code.js>
function math(node, _, context, safeOptions) {
const raw = node.value || ''
const tracker = track(safeOptions)
const sequence = '$'.repeat(Math.max(longestStreak(raw, '$') + 1, 2))
const exit = context.enter('mathFlow')
let value = tracker.move(sequence)
if (node.meta) {
const subexit = context.enter('mathFlowMeta')
value += tracker.move(
safe(context, node.meta, {
before: value,
after: '\n',
encode: ['$'],
...tracker.current()
})
)
subexit()
}
value += tracker.move('\n')
if (raw) {
value += tracker.move(raw + '\n')
}
value += tracker.move(sequence)
exit()
return value
}
/**
* @type {ToMarkdownHandle}
* @param {InlineMath} node
*/
// Note: fixing this code? Please also fix the similar code for inline code:
// <https://github.com/syntax-tree/mdast-util-to-markdown/blob/main/lib/handle/inline-code.js>
//
// To do: next major: rename `context` to state.
// To do: next major: use `state` (`safe`, `track`, `patternCompile`).
function inlineMath(node, _, context) {
let value = node.value || ''
let size = 1
if (!single) size++
// If there is a single dollar sign on its own in the math, use a fence of
// two.
// If there are two in a row, use one.
while (
new RegExp('(^|[^$])' + '\\$'.repeat(size) + '([^$]|$)').test(value)
) {
size++
}
const sequence = '$'.repeat(size)
// If this is not just spaces or eols (tabs dont count), and either the
// first and last character are a space or eol, or the first or last
// character are dollar signs, then pad with spaces.
if (
// Contains non-space.
/[^ \r\n]/.test(value) &&
// Starts with space and ends with space.
((/^[ \r\n]/.test(value) && /[ \r\n]$/.test(value)) ||
// Starts or ends with dollar.
/^\$|\$$/.test(value))
) {
value = ' ' + value + ' '
}
let index = -1
// We have a potential problem: certain characters after eols could result in
// blocks being seen.
// For example, if someone injected the string `'\n# b'`, then that would
// result in an ATX heading.
// We cant escape characters in `inlineMath`, but because eols are
// transformed to spaces when going from markdown to HTML anyway, we can swap
// them out.
while (++index < context.unsafe.length) {
const pattern = context.unsafe[index]
const expression = patternCompile(pattern)
/** @type {RegExpExecArray | null} */
let match
// Only look for `atBreak`s.
// Btw: note that `atBreak` patterns will always start the regex at LF or
// CR.
if (!pattern.atBreak) continue
while ((match = expression.exec(value))) {
let position = match.index
// Support CRLF (patterns only look for one of the characters).
if (
value.codePointAt(position) === 10 /* `\n` */ &&
value.codePointAt(position - 1) === 13 /* `\r` */
) {
position--
}
value = value.slice(0, position) + ' ' + value.slice(match.index + 1)
}
}
return sequence + value + sequence
}
/**
* @returns {string}
*/
function inlineMathPeek() {
return '$'
}
}

22
node_modules/mdast-util-math/license generated vendored Normal file
View File

@@ -0,0 +1,22 @@
(The MIT License)
Copyright (c) 2020 Titus Wormer <tituswormer@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,11 @@
/**
* Get the count of the longest repeating streak of `substring` in `value`.
*
* @param {string} value
* Content to search in.
* @param {string} substring
* Substring to look for, typically one character.
* @returns {number}
* Count of most frequent adjacent `substring`s in `value`.
*/
export function longestStreak(value: string, substring: string): number

View File

@@ -0,0 +1,36 @@
/**
* Get the count of the longest repeating streak of `substring` in `value`.
*
* @param {string} value
* Content to search in.
* @param {string} substring
* Substring to look for, typically one character.
* @returns {number}
* Count of most frequent adjacent `substring`s in `value`.
*/
export function longestStreak(value, substring) {
const source = String(value)
let index = source.indexOf(substring)
let expected = index
let count = 0
let max = 0
if (typeof substring !== 'string') {
throw new TypeError('Expected substring')
}
while (index !== -1) {
if (index === expected) {
if (++count > max) {
max = count
}
} else {
count = 1
}
expected = index + substring.length
index = source.indexOf(substring, expected)
}
return max
}

View File

@@ -0,0 +1,22 @@
(The MIT License)
Copyright (c) 2015 Titus Wormer <mailto:tituswormer@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,73 @@
{
"name": "longest-streak",
"version": "3.1.0",
"description": "Count the longest repeating streak of a substring",
"license": "MIT",
"keywords": [
"count",
"length",
"longest",
"repeating",
"streak",
"substring",
"character"
],
"repository": "wooorm/longest-streak",
"bugs": "https://github.com/wooorm/longest-streak/issues",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
},
"author": "Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)",
"contributors": [
"Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)"
],
"sideEffects": false,
"type": "module",
"main": "index.js",
"types": "index.d.ts",
"files": [
"index.d.ts",
"index.js"
],
"devDependencies": {
"@types/node": "^18.0.0",
"c8": "^7.0.0",
"prettier": "^2.0.0",
"remark-cli": "^11.0.0",
"remark-preset-wooorm": "^9.0.0",
"type-coverage": "^2.0.0",
"typescript": "^4.0.0",
"xo": "^0.52.0"
},
"scripts": {
"prepack": "npm run build && npm run format",
"build": "tsc --build --clean && tsc --build && type-coverage",
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
"test-api": "node --conditions development test.js",
"test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api",
"test": "npm run build && npm run format && npm run test-coverage"
},
"prettier": {
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"bracketSpacing": false,
"semi": false,
"trailingComma": "none"
},
"xo": {
"prettier": true
},
"remarkConfig": {
"plugins": [
"preset-wooorm"
]
},
"typeCoverage": {
"atLeast": 100,
"detail": true,
"strict": true,
"ignoreCatch": true
}
}

View File

@@ -0,0 +1,150 @@
# longest-streak
[![Build][build-badge]][build]
[![Coverage][coverage-badge]][coverage]
[![Downloads][downloads-badge]][downloads]
[![Size][size-badge]][size]
Get the count of the longest repeating streak of `substring` in `value`.
## Contents
* [What is this?](#what-is-this)
* [When should I use this?](#when-should-i-use-this)
* [Install](#install)
* [Use](#use)
* [API](#api)
* [`longestStreak(value, substring)`](#longeststreakvalue-substring)
* [Types](#types)
* [Compatibility](#compatibility)
* [Security](#security)
* [Related](#related)
* [Contribute](#contribute)
* [License](#license)
## What is this?
This is a tiny package that finds the count of the longest adjacent repeating
substring.
## When should I use this?
This package is rather niche.
I use it for serializing markdown ASTs (particularly fenced code and math).
You can use [`ccount`][ccount] if you need the total count of substrings
occuring in a value.
## Install
This package is [ESM only][esm].
In Node.js (version 14.14+, 16.0+), install with [npm][]:
```sh
npm install longest-streak
```
In Deno with [`esm.sh`][esmsh]:
```js
import {longestStreak} from 'https://esm.sh/longest-streak@3'
```
In browsers with [`esm.sh`][esmsh]:
```html
<script type="module">
import {longestStreak} from 'https://esm.sh/longest-streak@3?bundle'
</script>
```
## Use
```js
import {longestStreak} from 'longest-streak'
longestStreak('` foo `` bar `', '`') // => 2
```
## API
This package exports the identifier `longestStreak`.
There is no default export.
### `longestStreak(value, substring)`
Get the count of the longest repeating streak of `substring` in `value`.
###### Parameters
* `value` (`string`) — content to search in
* `substring` (`string`) — substring to look for, typically one character
###### Returns
Count of most frequent adjacent `substring`s in `value` (`number`).
## Types
This package is fully typed with [TypeScript][].
It exports no additional types.
## Compatibility
This package is at least compatible with all maintained versions of Node.js.
As of now, that is Node.js 14.14+ and 16.0+.
It also works in Deno and modern browsers.
## Security
This package is safe.
## Related
* [`wooorm/ccount`](https://github.com/wooorm/ccount)
— count the total number of `substring`s in `value`
* [`wooorm/direction`](https://github.com/wooorm/direction)
— detect directionality: left-to-right, right-to-left, or neutral
## Contribute
Yes please!
See [How to Contribute to Open Source][contribute].
## License
[MIT][license] © [Titus Wormer][author]
<!-- Definitions -->
[build-badge]: https://github.com/wooorm/longest-streak/workflows/main/badge.svg
[build]: https://github.com/wooorm/longest-streak/actions
[coverage-badge]: https://img.shields.io/codecov/c/github/wooorm/longest-streak.svg
[coverage]: https://codecov.io/github/wooorm/longest-streak
[downloads-badge]: https://img.shields.io/npm/dm/longest-streak.svg
[downloads]: https://www.npmjs.com/package/longest-streak
[size-badge]: https://img.shields.io/bundlephobia/minzip/longest-streak.svg
[size]: https://bundlephobia.com/result?p=longest-streak
[npm]: https://docs.npmjs.com/cli/install
[esmsh]: https://esm.sh
[license]: license
[author]: https://wooorm.com
[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
[typescript]: https://www.typescriptlang.org
[contribute]: https://opensource.guide/how-to-contribute/
[ccount]: https://github.com/wooorm/ccount

View File

@@ -0,0 +1 @@
export {phrasing} from './lib/index.js'

View File

@@ -0,0 +1 @@
export {phrasing} from './lib/index.js'

View File

@@ -0,0 +1,12 @@
/**
* Check if the given value is *phrasing content*.
*
* @param
* Thing to check, typically `Node`.
* @returns
* Whether `node` is phrasing content.
*/
export const phrasing: AssertPredicatePhrasing
export type PhrasingContent = import('mdast').PhrasingContent
export type AssertPredicatePhrasing =
import('unist-util-is').AssertPredicate<PhrasingContent>

View File

@@ -0,0 +1,31 @@
/**
* @typedef {import('mdast').PhrasingContent} PhrasingContent
* @typedef {import('unist-util-is').AssertPredicate<PhrasingContent>} AssertPredicatePhrasing
*/
import {convert} from 'unist-util-is'
/**
* Check if the given value is *phrasing content*.
*
* @param
* Thing to check, typically `Node`.
* @returns
* Whether `value` is phrasing content.
*/
export const phrasing = /** @type {AssertPredicatePhrasing} */ (
convert([
'break',
'delete',
'emphasis',
'footnote',
'footnoteReference',
'image',
'imageReference',
'inlineCode',
'link',
'linkReference',
'strong',
'text'
])
)

View File

@@ -0,0 +1,23 @@
(The MIT License)
Copyright (c) 2017 Titus Wormer <tituswormer@gmail.com>
Copyright (c) 2017 Victor Felder <victor@draft.li>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,78 @@
{
"name": "mdast-util-phrasing",
"version": "3.0.1",
"description": "mdast utility to check if a node is phrasing content",
"license": "MIT",
"keywords": [
"unist",
"mdast",
"mdast=util",
"util",
"utility",
"markdown",
"phrasing"
],
"repository": "syntax-tree/mdast-util-phrasing",
"bugs": "https://github.com/syntax-tree/mdast-util-phrasing/issues",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
},
"author": "Victor Felder <victor@draft.li> (https://draft.li)",
"contributors": [
"Victor Felder <victor@draft.li> (https://draft.li)",
"Titus Wormer <tituswormer@gmail.com> (https://wooorm.com)"
],
"sideEffects": false,
"type": "module",
"main": "index.js",
"types": "index.d.ts",
"files": [
"lib/",
"index.d.ts",
"index.js"
],
"dependencies": {
"@types/mdast": "^3.0.0",
"unist-util-is": "^5.0.0"
},
"devDependencies": {
"@types/node": "^18.0.0",
"c8": "^7.0.0",
"prettier": "^2.0.0",
"remark-cli": "^11.0.0",
"remark-preset-wooorm": "^9.0.0",
"type-coverage": "^2.0.0",
"typescript": "^4.0.0",
"xo": "^0.53.0"
},
"scripts": {
"prepack": "npm run build && npm run format",
"build": "tsc --build --clean && tsc --build && type-coverage",
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
"test-api": "node --conditions development test.js",
"test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api",
"test": "npm run build && npm run format && npm run test-coverage"
},
"prettier": {
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"bracketSpacing": false,
"semi": false,
"trailingComma": "none"
},
"xo": {
"prettier": true
},
"remarkConfig": {
"plugins": [
"preset-wooorm"
]
},
"typeCoverage": {
"atLeast": 100,
"detail": true,
"strict": true
}
}

View File

@@ -0,0 +1,195 @@
# mdast-util-phrasing
[![Build][build-badge]][build]
[![Coverage][coverage-badge]][coverage]
[![Downloads][downloads-badge]][downloads]
[![Size][size-badge]][size]
[![Sponsors][sponsors-badge]][collective]
[![Backers][backers-badge]][collective]
[![Chat][chat-badge]][chat]
[mdast][] utility to check if a node is phrasing content.
## Contents
* [What is this?](#what-is-this)
* [When should I use this?](#when-should-i-use-this)
* [Install](#install)
* [Use](#use)
* [API](#api)
* [`phrasing(value)`](#phrasingvalue)
* [Types](#types)
* [Compatibility](#compatibility)
* [Security](#security)
* [Related](#related)
* [Contribute](#contribute)
* [License](#license)
## What is this?
This package is a tiny utility to check that a given [node][] is [phrasing
content][phrasing].
## When should I use this?
This utility is typically useful if youre making other utilities.
It uses [`unist-util-is`][unist-util-is], which you can use for your own checks.
A different utility, [`hast-util-phrasing`][hast-util-phrasing], does the same
but on [hast][].
## Install
This package is [ESM only][esm].
In Node.js (version 14.14+ and 16.0+), install with [npm][]:
```sh
npm install mdast-util-phrasing
```
In Deno with [`esm.sh`][esmsh]:
```js
import {phrasing} from 'https://esm.sh/mdast-util-phrasing@3'
```
In browsers with [`esm.sh`][esmsh]:
```html
<script type="module">
import {phrasing} from 'https://esm.sh/mdast-util-phrasing@3?bundle'
</script>
```
## Use
```js
import {phrasing} from 'mdast-util-phrasing'
phrasing({type: 'paragraph', children: [{type: 'text', value: 'Alpha'}]})
// => false
phrasing({type: 'strong', children: [{type: 'text', value: 'Delta'}]})
// => true
```
## API
This package exports the identifier [`phrasing`][api-phrasing].
There is no default export.
### `phrasing(value)`
Check if the given value is *[phrasing content][phrasing]*.
###### Parameters
* `value` (`unknown`)
— thing to check, typically [`Node`][node]
###### Returns
Whether `value` is phrasing content (`boolean`).
## Types
This package is fully typed with [TypeScript][].
It does not export extra types.
## Compatibility
Projects maintained by the unified collective are compatible with all maintained
versions of Node.js.
As of now, that is Node.js 14.14+ and 16.0+.
Our projects sometimes work with older versions, but this is not guaranteed.
## Security
Use of `mdast-util-phrasing` does not involve **[hast][]**, user content, or
change the tree, so there are no openings for [cross-site scripting (XSS)][xss]
attacks.
## Related
* [`hast-util-phrasing`](https://github.com/syntax-tree/hast-util-phrasing)
— check if a hast node is phrasing content
* [`unist-util-is`](https://github.com/syntax-tree/unist-util-is)
— check if a node passes a test
## Contribute
See [`contributing.md`][contributing] in [`syntax-tree/.github`][health] for
ways to get started.
See [`support.md`][support] for ways to get help.
This project has a [code of conduct][coc].
By interacting with this repository, organization, or community you agree to
abide by its terms.
## License
[MIT][license] © [Victor Felder][author]
<!-- Definitions -->
[build-badge]: https://github.com/syntax-tree/mdast-util-phrasing/workflows/main/badge.svg
[build]: https://github.com/syntax-tree/mdast-util-phrasing/actions
[coverage-badge]: https://img.shields.io/codecov/c/github/syntax-tree/mdast-util-phrasing.svg
[coverage]: https://codecov.io/github/syntax-tree/mdast-util-phrasing
[downloads-badge]: https://img.shields.io/npm/dm/mdast-util-phrasing.svg
[downloads]: https://www.npmjs.com/package/mdast-util-phrasing
[size-badge]: https://img.shields.io/bundlephobia/minzip/mdast-util-phrasing.svg
[size]: https://bundlephobia.com/result?p=mdast-util-phrasing
[sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg
[backers-badge]: https://opencollective.com/unified/backers/badge.svg
[collective]: https://opencollective.com/unified
[chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg
[chat]: https://github.com/syntax-tree/unist/discussions
[npm]: https://docs.npmjs.com/cli/install
[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
[esmsh]: https://esm.sh
[typescript]: https://www.typescriptlang.org
[license]: license
[author]: https://draft.li
[health]: https://github.com/syntax-tree/.github
[contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md
[support]: https://github.com/syntax-tree/.github/blob/main/support.md
[coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md
[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
[hast]: https://github.com/syntax-tree/hast
[mdast]: https://github.com/syntax-tree/mdast
[node]: https://github.com/syntax-tree/mdast#nodes
[phrasing]: https://github.com/syntax-tree/mdast#phrasingcontent
[unist-util-is]: https://github.com/syntax-tree/unist-util-is
[hast-util-phrasing]: https://github.com/syntax-tree/hast-util-phrasing
[api-phrasing]: #phrasingvalue

View File

@@ -0,0 +1,327 @@
import type {Info, State} from './lib/types.js'
/**
* Interface of registered constructs.
*
* When working on extensions that use new constructs, extend the corresponding
* interface to register its name:
*
* ```ts
* declare module 'mdast-util-to-markdown' {
* interface ConstructNameMap {
* // Register a new construct name (value is used, key should match it).
* gfmStrikethrough: 'gfmStrikethrough'
* }
* }
* ```
*/
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export interface ConstructNameMap {
/**
* Whole autolink.
*
* ```markdown
* > | <https://example.com> and <admin@example.com>
* ^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
* ```
*/
autolink: 'autolink'
/**
* Whole block quote.
*
* ```markdown
* > | > a
* ^^^
* > | b
* ^
* ```
*/
blockquote: 'blockquote'
/**
* Whole code (indented).
*
* ```markdown
* ␠␠␠␠console.log(1)
* ^^^^^^^^^^^^^^^^^^
* ```
*/
codeIndented: 'codeIndented'
/**
* Whole code (fenced).
*
* ````markdown
* > | ```js
* ^^^^^
* > | console.log(1)
* ^^^^^^^^^^^^^^
* > | ```
* ^^^
* ````
*/
codeFenced: 'codeFenced'
/**
* Code (fenced) language, when fenced with grave accents.
*
* ````markdown
* > | ```js
* ^^
* | console.log(1)
* | ```
* ````
*/
codeFencedLangGraveAccent: 'codeFencedLangGraveAccent'
/**
* Code (fenced) language, when fenced with tildes.
*
* ````markdown
* > | ~~~js
* ^^
* | console.log(1)
* | ~~~
* ````
*/
codeFencedLangTilde: 'codeFencedLangTilde'
/**
* Code (fenced) meta string, when fenced with grave accents.
*
* ````markdown
* > | ```js eval
* ^^^^
* | console.log(1)
* | ```
* ````
*/
codeFencedMetaGraveAccent: 'codeFencedMetaGraveAccent'
/**
* Code (fenced) meta string, when fenced with tildes.
*
* ````markdown
* > | ~~~js eval
* ^^^^
* | console.log(1)
* | ~~~
* ````
*/
codeFencedMetaTilde: 'codeFencedMetaTilde'
/**
* Whole definition.
*
* ```markdown
* > | [a]: b "c"
* ^^^^^^^^^^
* ```
*/
definition: 'definition'
/**
* Destination (literal) (occurs in definition, image, link).
*
* ```markdown
* > | [a]: <b> "c"
* ^^^
* > | a ![b](<c> "d") e
* ^^^
* ```
*/
destinationLiteral: 'destinationLiteral'
/**
* Destination (raw) (occurs in definition, image, link).
*
* ```markdown
* > | [a]: b "c"
* ^
* > | a ![b](c "d") e
* ^
* ```
*/
destinationRaw: 'destinationRaw'
/**
* Emphasis.
*
* ```markdown
* > | *a*
* ^^^
* ```
*/
emphasis: 'emphasis'
/**
* Whole heading (atx).
*
* ```markdown
* > | # alpha
* ^^^^^^^
* ```
*/
headingAtx: 'headingAtx'
/**
* Whole heading (setext).
*
* ```markdown
* > | alpha
* ^^^^^
* > | =====
* ^^^^^
* ```
*/
headingSetext: 'headingSetext'
/**
* Whole image.
*
* ```markdown
* > | ![a](b)
* ^^^^^^^
* > | ![c]
* ^^^^
* ```
*/
image: 'image'
/**
* Whole image reference.
*
* ```markdown
* > | ![a]
* ^^^^
* ```
*/
imageReference: 'imageReference'
/**
* Label (occurs in definitions, image reference, image, link reference,
* link).
*
* ```markdown
* > | [a]: b "c"
* ^^^
* > | a [b] c
* ^^^
* > | a ![b][c] d
* ^^^^
* > | a [b](c) d
* ^^^
* ```
*/
label: 'label'
/**
* Whole link.
*
* ```markdown
* > | [a](b)
* ^^^^^^
* > | [c]
* ^^^
* ```
*/
link: 'link'
/**
* Whole link reference.
*
* ```markdown
* > | [a]
* ^^^
* ```
*/
linkReference: 'linkReference'
/**
* List.
*
* ```markdown
* > | * a
* ^^^
* > | 1. b
* ^^^^
* ```
*/
list: 'list'
/**
* List item.
*
* ```markdown
* > | * a
* ^^^
* > | 1. b
* ^^^^
* ```
*/
listItem: 'listItem'
/**
* Paragraph.
*
* ```markdown
* > | a b
* ^^^
* > | c.
* ^^
* ```
*/
paragraph: 'paragraph'
/**
* Phrasing (occurs in headings, paragraphs, etc).
*
* ```markdown
* > | a
* ^
* ```
*/
phrasing: 'phrasing'
/**
* Reference (occurs in image, link).
*
* ```markdown
* > | [a][]
* ^^
* ```
*/
reference: 'reference'
/**
* Strong.
*
* ```markdown
* > | **a**
* ^^^^^
* ```
*/
strong: 'strong'
/**
* Title using single quotes (occurs in definition, image, link).
*
* ```markdown
* > | [a](b 'c')
* ^^^
* ```
*/
titleApostrophe: 'titleApostrophe'
/**
* Title using double quotes (occurs in definition, image, link).
*
* ```markdown
* > | [a](b "c")
* ^^^
* ```
*/
titleQuote: 'titleQuote'
}
/**
* Construct names for things generated by `mdast-util-to-markdown`.
*
* This is an enum of strings, each being a semantic label, useful to know when
* serializing whether were for example in a double (`"`) or single (`'`)
* quoted title.
*/
export type ConstructName = ConstructNameMap[keyof ConstructNameMap]
export {toMarkdown} from './lib/index.js'
export {handle as defaultHandlers} from './lib/handle/index.js'
export type {
Handle,
Handlers,
Info,
Join,
Map,
Options,
SafeConfig,
State,
Tracker,
Unsafe
} from './lib/types.js'
// Deprecated.
export type SafeOptions = Info
export type Context = State

View File

@@ -0,0 +1,2 @@
export {toMarkdown} from './lib/index.js'
export {handle as defaultHandlers} from './lib/handle/index.js'

View File

@@ -0,0 +1,12 @@
/**
* @typedef {import('./types.js').Options} Options
* @typedef {import('./types.js').State} State
*/
/**
* @param {State} base
* @param {Options} extension
* @returns {State}
*/
export function configure(base: State, extension: Options): State
export type Options = import('./types.js').Options
export type State = import('./types.js').State

View File

@@ -0,0 +1,39 @@
/**
* @typedef {import('./types.js').Options} Options
* @typedef {import('./types.js').State} State
*/
/**
* @param {State} base
* @param {Options} extension
* @returns {State}
*/
export function configure(base, extension) {
let index = -1
/** @type {keyof Options} */
let key
// First do subextensions.
if (extension.extensions) {
while (++index < extension.extensions.length) {
configure(base, extension.extensions[index])
}
}
for (key in extension) {
if (key === 'extensions') {
// Empty.
} else if (key === 'unsafe' || key === 'join') {
/* c8 ignore next 2 */
// @ts-expect-error: hush.
base[key] = [...(base[key] || []), ...(extension[key] || [])]
} else if (key === 'handlers') {
base[key] = Object.assign(base[key], extension[key] || {})
} else {
// @ts-expect-error: hush.
base.options[key] = extension[key]
}
}
return base
}

View File

@@ -0,0 +1,25 @@
/**
* @typedef {import('mdast').Blockquote} Blockquote
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
* @typedef {import('../types.js').Map} Map
*/
/**
* @param {Blockquote} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function blockquote(
node: Blockquote,
_: Parent | undefined,
state: State,
info: Info
): string
export type Blockquote = import('mdast').Blockquote
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info
export type Map = import('../types.js').Map

View File

@@ -0,0 +1,32 @@
/**
* @typedef {import('mdast').Blockquote} Blockquote
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
* @typedef {import('../types.js').Map} Map
*/
/**
* @param {Blockquote} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function blockquote(node, _, state, info) {
const exit = state.enter('blockquote')
const tracker = state.createTracker(info)
tracker.move('> ')
tracker.shift(2)
const value = state.indentLines(
state.containerFlow(node, tracker.current()),
map
)
exit()
return value
}
/** @type {Map} */
function map(line, _, blank) {
return '>' + (blank ? '' : ' ') + line
}

View File

@@ -0,0 +1,17 @@
/**
* @param {Break} _
* @param {Parent | undefined} _1
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function hardBreak(
_: Break,
_1: Parent | undefined,
state: State,
info: Info
): string
export type Break = import('mdast').Break
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info

View File

@@ -0,0 +1,32 @@
/**
* @typedef {import('mdast').Break} Break
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
*/
import {patternInScope} from '../util/pattern-in-scope.js'
/**
* @param {Break} _
* @param {Parent | undefined} _1
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function hardBreak(_, _1, state, info) {
let index = -1
while (++index < state.unsafe.length) {
// If we cant put eols in this construct (setext headings, tables), use a
// space instead.
if (
state.unsafe[index].character === '\n' &&
patternInScope(state.stack, state.unsafe[index])
) {
return /[ \t]/.test(info.before) ? '' : ' '
}
}
return '\\\n'
}

View File

@@ -0,0 +1,18 @@
/**
* @param {Code} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function code(
node: Code,
_: Parent | undefined,
state: State,
info: Info
): string
export type Code = import('mdast').Code
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info
export type Map = import('../types.js').Map

View File

@@ -0,0 +1,78 @@
/**
* @typedef {import('mdast').Code} Code
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
* @typedef {import('../types.js').Map} Map
*/
import {longestStreak} from 'longest-streak'
import {formatCodeAsIndented} from '../util/format-code-as-indented.js'
import {checkFence} from '../util/check-fence.js'
/**
* @param {Code} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function code(node, _, state, info) {
const marker = checkFence(state)
const raw = node.value || ''
const suffix = marker === '`' ? 'GraveAccent' : 'Tilde'
if (formatCodeAsIndented(node, state)) {
const exit = state.enter('codeIndented')
const value = state.indentLines(raw, map)
exit()
return value
}
const tracker = state.createTracker(info)
const sequence = marker.repeat(Math.max(longestStreak(raw, marker) + 1, 3))
const exit = state.enter('codeFenced')
let value = tracker.move(sequence)
if (node.lang) {
const subexit = state.enter(`codeFencedLang${suffix}`)
value += tracker.move(
state.safe(node.lang, {
before: value,
after: ' ',
encode: ['`'],
...tracker.current()
})
)
subexit()
}
if (node.lang && node.meta) {
const subexit = state.enter(`codeFencedMeta${suffix}`)
value += tracker.move(' ')
value += tracker.move(
state.safe(node.meta, {
before: value,
after: '\n',
encode: ['`'],
...tracker.current()
})
)
subexit()
}
value += tracker.move('\n')
if (raw) {
value += tracker.move(raw + '\n')
}
value += tracker.move(sequence)
exit()
return value
}
/** @type {Map} */
function map(line, _, blank) {
return (blank ? '' : ' ') + line
}

View File

@@ -0,0 +1,17 @@
/**
* @param {Definition} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function definition(
node: Definition,
_: Parent | undefined,
state: State,
info: Info
): string
export type Definition = import('mdast').Definition
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info

View File

@@ -0,0 +1,78 @@
/**
* @typedef {import('mdast').Definition} Definition
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
*/
import {checkQuote} from '../util/check-quote.js'
/**
* @param {Definition} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function definition(node, _, state, info) {
const quote = checkQuote(state)
const suffix = quote === '"' ? 'Quote' : 'Apostrophe'
const exit = state.enter('definition')
let subexit = state.enter('label')
const tracker = state.createTracker(info)
let value = tracker.move('[')
value += tracker.move(
state.safe(state.associationId(node), {
before: value,
after: ']',
...tracker.current()
})
)
value += tracker.move(']: ')
subexit()
if (
// If theres no url, or…
!node.url ||
// If there are control characters or whitespace.
/[\0- \u007F]/.test(node.url)
) {
subexit = state.enter('destinationLiteral')
value += tracker.move('<')
value += tracker.move(
state.safe(node.url, {before: value, after: '>', ...tracker.current()})
)
value += tracker.move('>')
} else {
// No whitespace, raw is prettier.
subexit = state.enter('destinationRaw')
value += tracker.move(
state.safe(node.url, {
before: value,
after: node.title ? ' ' : '\n',
...tracker.current()
})
)
}
subexit()
if (node.title) {
subexit = state.enter(`title${suffix}`)
value += tracker.move(' ' + quote)
value += tracker.move(
state.safe(node.title, {
before: value,
after: quote,
...tracker.current()
})
)
value += tracker.move(quote)
subexit()
}
exit()
return value
}

View File

@@ -0,0 +1,32 @@
/**
* @param {Emphasis} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function emphasis(
node: Emphasis,
_: Parent | undefined,
state: State,
info: Info
): string
export namespace emphasis {
export {emphasisPeek as peek}
}
export type Emphasis = import('mdast').Emphasis
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info
/**
* @param {Emphasis} _
* @param {Parent | undefined} _1
* @param {State} state
* @returns {string}
*/
declare function emphasisPeek(
_: Emphasis,
_1: Parent | undefined,
state: State
): string
export {}

View File

@@ -0,0 +1,48 @@
/**
* @typedef {import('mdast').Emphasis} Emphasis
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
*/
import {checkEmphasis} from '../util/check-emphasis.js'
emphasis.peek = emphasisPeek
// To do: there are cases where emphasis cannot “form” depending on the
// previous or next character of sequences.
// Theres no way around that though, except for injecting zero-width stuff.
// Do we need to safeguard against that?
/**
* @param {Emphasis} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function emphasis(node, _, state, info) {
const marker = checkEmphasis(state)
const exit = state.enter('emphasis')
const tracker = state.createTracker(info)
let value = tracker.move(marker)
value += tracker.move(
state.containerPhrasing(node, {
before: value,
after: marker,
...tracker.current()
})
)
value += tracker.move(marker)
exit()
return value
}
/**
* @param {Emphasis} _
* @param {Parent | undefined} _1
* @param {State} state
* @returns {string}
*/
function emphasisPeek(_, _1, state) {
return state.options.emphasis || '*'
}

View File

@@ -0,0 +1,17 @@
/**
* @param {Heading} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function heading(
node: Heading,
_: Parent | undefined,
state: State,
info: Info
): string
export type Heading = import('mdast').Heading
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info

View File

@@ -0,0 +1,80 @@
/**
* @typedef {import('mdast').Heading} Heading
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
*/
import {formatHeadingAsSetext} from '../util/format-heading-as-setext.js'
/**
* @param {Heading} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function heading(node, _, state, info) {
const rank = Math.max(Math.min(6, node.depth || 1), 1)
const tracker = state.createTracker(info)
if (formatHeadingAsSetext(node, state)) {
const exit = state.enter('headingSetext')
const subexit = state.enter('phrasing')
const value = state.containerPhrasing(node, {
...tracker.current(),
before: '\n',
after: '\n'
})
subexit()
exit()
return (
value +
'\n' +
(rank === 1 ? '=' : '-').repeat(
// The whole size…
value.length -
// Minus the position of the character after the last EOL (or
// 0 if there is none)…
(Math.max(value.lastIndexOf('\r'), value.lastIndexOf('\n')) + 1)
)
)
}
const sequence = '#'.repeat(rank)
const exit = state.enter('headingAtx')
const subexit = state.enter('phrasing')
// Note: for proper tracking, we should reset the output positions when there
// is no content returned, because then the space is not output.
// Practically, in that case, there is no content, so it doesnt matter that
// weve tracked one too many characters.
tracker.move(sequence + ' ')
let value = state.containerPhrasing(node, {
before: '# ',
after: '\n',
...tracker.current()
})
if (/^[\t ]/.test(value)) {
// To do: what effect has the character reference on tracking?
value =
'&#x' +
value.charCodeAt(0).toString(16).toUpperCase() +
';' +
value.slice(1)
}
value = value ? sequence + ' ' + value : sequence
if (state.options.closeAtx) {
value += ' ' + sequence
}
subexit()
exit()
return value
}

View File

@@ -0,0 +1,14 @@
/**
* @param {HTML} node
* @returns {string}
*/
export function html(node: HTML): string
export namespace html {
export {htmlPeek as peek}
}
export type HTML = import('mdast').HTML
/**
* @returns {string}
*/
declare function htmlPeek(): string
export {}

View File

@@ -0,0 +1,20 @@
/**
* @typedef {import('mdast').HTML} HTML
*/
html.peek = htmlPeek
/**
* @param {HTML} node
* @returns {string}
*/
export function html(node) {
return node.value || ''
}
/**
* @returns {string}
*/
function htmlPeek() {
return '<'
}

View File

@@ -0,0 +1,25 @@
/**
* @param {ImageReference} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function imageReference(
node: ImageReference,
_: Parent | undefined,
state: State,
info: Info
): string
export namespace imageReference {
export {imageReferencePeek as peek}
}
export type ImageReference = import('mdast').ImageReference
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info
/**
* @returns {string}
*/
declare function imageReferencePeek(): string
export {}

View File

@@ -0,0 +1,65 @@
/**
* @typedef {import('mdast').ImageReference} ImageReference
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
*/
imageReference.peek = imageReferencePeek
/**
* @param {ImageReference} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function imageReference(node, _, state, info) {
const type = node.referenceType
const exit = state.enter('imageReference')
let subexit = state.enter('label')
const tracker = state.createTracker(info)
let value = tracker.move('![')
const alt = state.safe(node.alt, {
before: value,
after: ']',
...tracker.current()
})
value += tracker.move(alt + '][')
subexit()
// Hide the fact that were in phrasing, because escapes dont work.
const stack = state.stack
state.stack = []
subexit = state.enter('reference')
// Note: for proper tracking, we should reset the output positions when we end
// up making a `shortcut` reference, because then there is no brace output.
// Practically, in that case, there is no content, so it doesnt matter that
// weve tracked one too many characters.
const reference = state.safe(state.associationId(node), {
before: value,
after: ']',
...tracker.current()
})
subexit()
state.stack = stack
exit()
if (type === 'full' || !alt || alt !== reference) {
value += tracker.move(reference + ']')
} else if (type === 'shortcut') {
// Remove the unwanted `[`.
value = value.slice(0, -1)
} else {
value += tracker.move(']')
}
return value
}
/**
* @returns {string}
*/
function imageReferencePeek() {
return '!'
}

View File

@@ -0,0 +1,25 @@
/**
* @param {Image} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function image(
node: Image,
_: Parent | undefined,
state: State,
info: Info
): string
export namespace image {
export {imagePeek as peek}
}
export type Image = import('mdast').Image
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info
/**
* @returns {string}
*/
declare function imagePeek(): string
export {}

View File

@@ -0,0 +1,84 @@
/**
* @typedef {import('mdast').Image} Image
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
*/
import {checkQuote} from '../util/check-quote.js'
image.peek = imagePeek
/**
* @param {Image} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function image(node, _, state, info) {
const quote = checkQuote(state)
const suffix = quote === '"' ? 'Quote' : 'Apostrophe'
const exit = state.enter('image')
let subexit = state.enter('label')
const tracker = state.createTracker(info)
let value = tracker.move('![')
value += tracker.move(
state.safe(node.alt, {before: value, after: ']', ...tracker.current()})
)
value += tracker.move('](')
subexit()
if (
// If theres no url but there is a title…
(!node.url && node.title) ||
// If there are control characters or whitespace.
/[\0- \u007F]/.test(node.url)
) {
subexit = state.enter('destinationLiteral')
value += tracker.move('<')
value += tracker.move(
state.safe(node.url, {before: value, after: '>', ...tracker.current()})
)
value += tracker.move('>')
} else {
// No whitespace, raw is prettier.
subexit = state.enter('destinationRaw')
value += tracker.move(
state.safe(node.url, {
before: value,
after: node.title ? ' ' : ')',
...tracker.current()
})
)
}
subexit()
if (node.title) {
subexit = state.enter(`title${suffix}`)
value += tracker.move(' ' + quote)
value += tracker.move(
state.safe(node.title, {
before: value,
after: quote,
...tracker.current()
})
)
value += tracker.move(quote)
subexit()
}
value += tracker.move(')')
exit()
return value
}
/**
* @returns {string}
*/
function imagePeek() {
return '!'
}

View File

@@ -0,0 +1,41 @@
export namespace handle {
export {blockquote}
export {hardBreak as break}
export {code}
export {definition}
export {emphasis}
export {hardBreak}
export {heading}
export {html}
export {image}
export {imageReference}
export {inlineCode}
export {link}
export {linkReference}
export {list}
export {listItem}
export {paragraph}
export {root}
export {strong}
export {text}
export {thematicBreak}
}
import {blockquote} from './blockquote.js'
import {hardBreak} from './break.js'
import {code} from './code.js'
import {definition} from './definition.js'
import {emphasis} from './emphasis.js'
import {heading} from './heading.js'
import {html} from './html.js'
import {image} from './image.js'
import {imageReference} from './image-reference.js'
import {inlineCode} from './inline-code.js'
import {link} from './link.js'
import {linkReference} from './link-reference.js'
import {list} from './list.js'
import {listItem} from './list-item.js'
import {paragraph} from './paragraph.js'
import {root} from './root.js'
import {strong} from './strong.js'
import {text} from './text.js'
import {thematicBreak} from './thematic-break.js'

View File

@@ -0,0 +1,45 @@
import {blockquote} from './blockquote.js'
import {hardBreak} from './break.js'
import {code} from './code.js'
import {definition} from './definition.js'
import {emphasis} from './emphasis.js'
import {heading} from './heading.js'
import {html} from './html.js'
import {image} from './image.js'
import {imageReference} from './image-reference.js'
import {inlineCode} from './inline-code.js'
import {link} from './link.js'
import {linkReference} from './link-reference.js'
import {list} from './list.js'
import {listItem} from './list-item.js'
import {paragraph} from './paragraph.js'
import {root} from './root.js'
import {strong} from './strong.js'
import {text} from './text.js'
import {thematicBreak} from './thematic-break.js'
/**
* Default (CommonMark) handlers.
*/
export const handle = {
blockquote,
break: hardBreak,
code,
definition,
emphasis,
hardBreak,
heading,
html,
image,
imageReference,
inlineCode,
link,
linkReference,
list,
listItem,
paragraph,
root,
strong,
text,
thematicBreak
}

View File

@@ -0,0 +1,22 @@
/**
* @param {InlineCode} node
* @param {Parent | undefined} _
* @param {State} state
* @returns {string}
*/
export function inlineCode(
node: InlineCode,
_: Parent | undefined,
state: State
): string
export namespace inlineCode {
export {inlineCodePeek as peek}
}
export type InlineCode = import('mdast').InlineCode
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
/**
* @returns {string}
*/
declare function inlineCodePeek(): string
export {}

View File

@@ -0,0 +1,79 @@
/**
* @typedef {import('mdast').InlineCode} InlineCode
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
*/
import {patternCompile} from '../util/pattern-compile.js'
inlineCode.peek = inlineCodePeek
/**
* @param {InlineCode} node
* @param {Parent | undefined} _
* @param {State} state
* @returns {string}
*/
export function inlineCode(node, _, state) {
let value = node.value || ''
let sequence = '`'
let index = -1
// If there is a single grave accent on its own in the code, use a fence of
// two.
// If there are two in a row, use one.
while (new RegExp('(^|[^`])' + sequence + '([^`]|$)').test(value)) {
sequence += '`'
}
// If this is not just spaces or eols (tabs dont count), and either the
// first or last character are a space, eol, or tick, then pad with spaces.
if (
/[^ \r\n]/.test(value) &&
((/^[ \r\n]/.test(value) && /[ \r\n]$/.test(value)) || /^`|`$/.test(value))
) {
value = ' ' + value + ' '
}
// We have a potential problem: certain characters after eols could result in
// blocks being seen.
// For example, if someone injected the string `'\n# b'`, then that would
// result in an ATX heading.
// We cant escape characters in `inlineCode`, but because eols are
// transformed to spaces when going from markdown to HTML anyway, we can swap
// them out.
while (++index < state.unsafe.length) {
const pattern = state.unsafe[index]
const expression = patternCompile(pattern)
/** @type {RegExpExecArray | null} */
let match
// Only look for `atBreak`s.
// Btw: note that `atBreak` patterns will always start the regex at LF or
// CR.
if (!pattern.atBreak) continue
while ((match = expression.exec(value))) {
let position = match.index
// Support CRLF (patterns only look for one of the characters).
if (
value.charCodeAt(position) === 10 /* `\n` */ &&
value.charCodeAt(position - 1) === 13 /* `\r` */
) {
position--
}
value = value.slice(0, position) + ' ' + value.slice(match.index + 1)
}
}
return sequence + value + sequence
}
/**
* @returns {string}
*/
function inlineCodePeek() {
return '`'
}

View File

@@ -0,0 +1,25 @@
/**
* @param {LinkReference} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function linkReference(
node: LinkReference,
_: Parent | undefined,
state: State,
info: Info
): string
export namespace linkReference {
export {linkReferencePeek as peek}
}
export type LinkReference = import('mdast').LinkReference
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info
/**
* @returns {string}
*/
declare function linkReferencePeek(): string
export {}

View File

@@ -0,0 +1,65 @@
/**
* @typedef {import('mdast').LinkReference} LinkReference
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
*/
linkReference.peek = linkReferencePeek
/**
* @param {LinkReference} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function linkReference(node, _, state, info) {
const type = node.referenceType
const exit = state.enter('linkReference')
let subexit = state.enter('label')
const tracker = state.createTracker(info)
let value = tracker.move('[')
const text = state.containerPhrasing(node, {
before: value,
after: ']',
...tracker.current()
})
value += tracker.move(text + '][')
subexit()
// Hide the fact that were in phrasing, because escapes dont work.
const stack = state.stack
state.stack = []
subexit = state.enter('reference')
// Note: for proper tracking, we should reset the output positions when we end
// up making a `shortcut` reference, because then there is no brace output.
// Practically, in that case, there is no content, so it doesnt matter that
// weve tracked one too many characters.
const reference = state.safe(state.associationId(node), {
before: value,
after: ']',
...tracker.current()
})
subexit()
state.stack = stack
exit()
if (type === 'full' || !text || text !== reference) {
value += tracker.move(reference + ']')
} else if (type === 'shortcut') {
// Remove the unwanted `[`.
value = value.slice(0, -1)
} else {
value += tracker.move(']')
}
return value
}
/**
* @returns {string}
*/
function linkReferencePeek() {
return '['
}

View File

@@ -0,0 +1,33 @@
/**
* @param {Link} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function link(
node: Link,
_: Parent | undefined,
state: State,
info: Info
): string
export namespace link {
export {linkPeek as peek}
}
export type Link = import('mdast').Link
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info
export type Exit = import('../types.js').Exit
/**
* @param {Link} node
* @param {Parent | undefined} _
* @param {State} state
* @returns {string}
*/
declare function linkPeek(
node: Link,
_: Parent | undefined,
state: State
): string
export {}

View File

@@ -0,0 +1,116 @@
/**
* @typedef {import('mdast').Link} Link
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
* @typedef {import('../types.js').Exit} Exit
*/
import {checkQuote} from '../util/check-quote.js'
import {formatLinkAsAutolink} from '../util/format-link-as-autolink.js'
link.peek = linkPeek
/**
* @param {Link} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function link(node, _, state, info) {
const quote = checkQuote(state)
const suffix = quote === '"' ? 'Quote' : 'Apostrophe'
const tracker = state.createTracker(info)
/** @type {Exit} */
let exit
/** @type {Exit} */
let subexit
if (formatLinkAsAutolink(node, state)) {
// Hide the fact that were in phrasing, because escapes dont work.
const stack = state.stack
state.stack = []
exit = state.enter('autolink')
let value = tracker.move('<')
value += tracker.move(
state.containerPhrasing(node, {
before: value,
after: '>',
...tracker.current()
})
)
value += tracker.move('>')
exit()
state.stack = stack
return value
}
exit = state.enter('link')
subexit = state.enter('label')
let value = tracker.move('[')
value += tracker.move(
state.containerPhrasing(node, {
before: value,
after: '](',
...tracker.current()
})
)
value += tracker.move('](')
subexit()
if (
// If theres no url but there is a title…
(!node.url && node.title) ||
// If there are control characters or whitespace.
/[\0- \u007F]/.test(node.url)
) {
subexit = state.enter('destinationLiteral')
value += tracker.move('<')
value += tracker.move(
state.safe(node.url, {before: value, after: '>', ...tracker.current()})
)
value += tracker.move('>')
} else {
// No whitespace, raw is prettier.
subexit = state.enter('destinationRaw')
value += tracker.move(
state.safe(node.url, {
before: value,
after: node.title ? ' ' : ')',
...tracker.current()
})
)
}
subexit()
if (node.title) {
subexit = state.enter(`title${suffix}`)
value += tracker.move(' ' + quote)
value += tracker.move(
state.safe(node.title, {
before: value,
after: quote,
...tracker.current()
})
)
value += tracker.move(quote)
subexit()
}
value += tracker.move(')')
exit()
return value
}
/**
* @param {Link} node
* @param {Parent | undefined} _
* @param {State} state
* @returns {string}
*/
function linkPeek(node, _, state) {
return formatLinkAsAutolink(node, state) ? '<' : '['
}

View File

@@ -0,0 +1,18 @@
/**
* @param {ListItem} node
* @param {Parent | undefined} parent
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function listItem(
node: ListItem,
parent: Parent | undefined,
state: State,
info: Info
): string
export type ListItem = import('mdast').ListItem
export type Map = import('../types.js').Map
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info

View File

@@ -0,0 +1,65 @@
/**
* @typedef {import('mdast').ListItem} ListItem
* @typedef {import('../types.js').Map} Map
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
*/
import {checkBullet} from '../util/check-bullet.js'
import {checkListItemIndent} from '../util/check-list-item-indent.js'
/**
* @param {ListItem} node
* @param {Parent | undefined} parent
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function listItem(node, parent, state, info) {
const listItemIndent = checkListItemIndent(state)
let bullet = state.bulletCurrent || checkBullet(state)
// Add the marker value for ordered lists.
if (parent && parent.type === 'list' && parent.ordered) {
bullet =
(typeof parent.start === 'number' && parent.start > -1
? parent.start
: 1) +
(state.options.incrementListMarker === false
? 0
: parent.children.indexOf(node)) +
bullet
}
let size = bullet.length + 1
if (
listItemIndent === 'tab' ||
(listItemIndent === 'mixed' &&
((parent && parent.type === 'list' && parent.spread) || node.spread))
) {
size = Math.ceil(size / 4) * 4
}
const tracker = state.createTracker(info)
tracker.move(bullet + ' '.repeat(size - bullet.length))
tracker.shift(size)
const exit = state.enter('listItem')
const value = state.indentLines(
state.containerFlow(node, tracker.current()),
map
)
exit()
return value
/** @type {Map} */
function map(line, index, blank) {
if (index) {
return (blank ? '' : ' '.repeat(size)) + line
}
return (blank ? bullet : bullet + ' '.repeat(size - bullet.length)) + line
}
}

View File

@@ -0,0 +1,17 @@
/**
* @param {List} node
* @param {Parent | undefined} parent
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function list(
node: List,
parent: Parent | undefined,
state: State,
info: Info
): string
export type List = import('mdast').List
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info

View File

@@ -0,0 +1,113 @@
/**
* @typedef {import('mdast').List} List
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
*/
import {checkBullet} from '../util/check-bullet.js'
import {checkBulletOther} from '../util/check-bullet-other.js'
import {checkBulletOrdered} from '../util/check-bullet-ordered.js'
import {checkBulletOrderedOther} from '../util/check-bullet-ordered-other.js'
import {checkRule} from '../util/check-rule.js'
/**
* @param {List} node
* @param {Parent | undefined} parent
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function list(node, parent, state, info) {
const exit = state.enter('list')
const bulletCurrent = state.bulletCurrent
/** @type {string} */
let bullet = node.ordered ? checkBulletOrdered(state) : checkBullet(state)
/** @type {string} */
const bulletOther = node.ordered
? checkBulletOrderedOther(state)
: checkBulletOther(state)
const bulletLastUsed = state.bulletLastUsed
let useDifferentMarker = false
if (
parent &&
// Explicit `other` set.
(node.ordered
? state.options.bulletOrderedOther
: state.options.bulletOther) &&
bulletLastUsed &&
bullet === bulletLastUsed
) {
useDifferentMarker = true
}
if (!node.ordered) {
const firstListItem = node.children ? node.children[0] : undefined
// If theres an empty first list item directly in two list items,
// we have to use a different bullet:
//
// ```markdown
// * - *
// ```
//
// …because otherwise it would become one big thematic break.
if (
// Bullet could be used as a thematic break marker:
(bullet === '*' || bullet === '-') &&
// Empty first list item:
firstListItem &&
(!firstListItem.children || !firstListItem.children[0]) &&
// Directly in two other list items:
state.stack[state.stack.length - 1] === 'list' &&
state.stack[state.stack.length - 2] === 'listItem' &&
state.stack[state.stack.length - 3] === 'list' &&
state.stack[state.stack.length - 4] === 'listItem' &&
// That are each the first child.
state.indexStack[state.indexStack.length - 1] === 0 &&
state.indexStack[state.indexStack.length - 2] === 0 &&
state.indexStack[state.indexStack.length - 3] === 0
) {
useDifferentMarker = true
}
// If theres a thematic break at the start of the first list item,
// we have to use a different bullet:
//
// ```markdown
// * ---
// ```
//
// …because otherwise it would become one big thematic break.
if (checkRule(state) === bullet && firstListItem) {
let index = -1
while (++index < node.children.length) {
const item = node.children[index]
if (
item &&
item.type === 'listItem' &&
item.children &&
item.children[0] &&
item.children[0].type === 'thematicBreak'
) {
useDifferentMarker = true
break
}
}
}
}
if (useDifferentMarker) {
bullet = bulletOther
}
state.bulletCurrent = bullet
const value = state.containerFlow(node, info)
state.bulletLastUsed = bullet
state.bulletCurrent = bulletCurrent
exit()
return value
}

View File

@@ -0,0 +1,23 @@
/**
* @typedef {import('mdast').Paragraph} Paragraph
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
*/
/**
* @param {Paragraph} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function paragraph(
node: Paragraph,
_: Parent | undefined,
state: State,
info: Info
): string
export type Paragraph = import('mdast').Paragraph
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info

View File

@@ -0,0 +1,22 @@
/**
* @typedef {import('mdast').Paragraph} Paragraph
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
*/
/**
* @param {Paragraph} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function paragraph(node, _, state, info) {
const exit = state.enter('paragraph')
const subexit = state.enter('phrasing')
const value = state.containerPhrasing(node, info)
subexit()
exit()
return value
}

View File

@@ -0,0 +1,17 @@
/**
* @param {Root} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function root(
node: Root,
_: Parent | undefined,
state: State,
info: Info
): string
export type Root = import('mdast').Root
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info

View File

@@ -0,0 +1,23 @@
/**
* @typedef {import('mdast').Root} Root
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
*/
import {phrasing} from 'mdast-util-phrasing'
/**
* @param {Root} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function root(node, _, state, info) {
// Note: `html` nodes are ambiguous.
const hasPhrasing = node.children.some((d) => phrasing(d))
const fn = hasPhrasing ? state.containerPhrasing : state.containerFlow
// @ts-expect-error: `root`s are supposed to have one type of content
return fn.call(state, node, info)
}

View File

@@ -0,0 +1,32 @@
/**
* @param {Strong} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function strong(
node: Strong,
_: Parent | undefined,
state: State,
info: Info
): string
export namespace strong {
export {strongPeek as peek}
}
export type Strong = import('mdast').Strong
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info
/**
* @param {Strong} _
* @param {Parent | undefined} _1
* @param {State} state
* @returns {string}
*/
declare function strongPeek(
_: Strong,
_1: Parent | undefined,
state: State
): string
export {}

View File

@@ -0,0 +1,48 @@
/**
* @typedef {import('mdast').Strong} Strong
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
*/
import {checkStrong} from '../util/check-strong.js'
strong.peek = strongPeek
// To do: there are cases where emphasis cannot “form” depending on the
// previous or next character of sequences.
// Theres no way around that though, except for injecting zero-width stuff.
// Do we need to safeguard against that?
/**
* @param {Strong} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function strong(node, _, state, info) {
const marker = checkStrong(state)
const exit = state.enter('strong')
const tracker = state.createTracker(info)
let value = tracker.move(marker + marker)
value += tracker.move(
state.containerPhrasing(node, {
before: value,
after: marker,
...tracker.current()
})
)
value += tracker.move(marker + marker)
exit()
return value
}
/**
* @param {Strong} _
* @param {Parent | undefined} _1
* @param {State} state
* @returns {string}
*/
function strongPeek(_, _1, state) {
return state.options.strong || '*'
}

View File

@@ -0,0 +1,23 @@
/**
* @typedef {import('mdast').Text} Text
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
*/
/**
* @param {Text} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function text(
node: Text,
_: Parent | undefined,
state: State,
info: Info
): string
export type Text = import('mdast').Text
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type Info = import('../types.js').Info

View File

@@ -0,0 +1,17 @@
/**
* @typedef {import('mdast').Text} Text
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Info} Info
*/
/**
* @param {Text} node
* @param {Parent | undefined} _
* @param {State} state
* @param {Info} info
* @returns {string}
*/
export function text(node, _, state, info) {
return state.safe(node.value, info)
}

View File

@@ -0,0 +1,14 @@
/**
* @param {ThematicBreak} _
* @param {Parent | undefined} _1
* @param {State} state
* @returns {string}
*/
export function thematicBreak(
_: ThematicBreak,
_1: Parent | undefined,
state: State
): string
export type ThematicBreak = import('mdast').ThematicBreak
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State

View File

@@ -0,0 +1,22 @@
/**
* @typedef {import('mdast').ThematicBreak} ThematicBreak
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
*/
import {checkRuleRepetition} from '../util/check-rule-repetition.js'
import {checkRule} from '../util/check-rule.js'
/**
* @param {ThematicBreak} _
* @param {Parent | undefined} _1
* @param {State} state
* @returns {string}
*/
export function thematicBreak(_, _1, state) {
const value = (
checkRule(state) + (state.options.ruleSpaces ? ' ' : '')
).repeat(checkRuleRepetition(state))
return state.options.ruleSpaces ? value.slice(0, -1) : value
}

View File

@@ -0,0 +1,25 @@
/**
* Turn an mdast syntax tree into markdown.
*
* @param {Node} tree
* Tree to serialize.
* @param {Options} [options]
* Configuration (optional).
* @returns {string}
* Serialized markdown representing `tree`.
*/
export function toMarkdown(
tree: Node,
options?: import('./types.js').Options | undefined
): string
export type Enter = import('./types.js').Enter
export type Info = import('./types.js').Info
export type Join = import('./types.js').Join
export type FlowContent = import('./types.js').FlowContent
export type Node = import('./types.js').Node
export type Options = import('./types.js').Options
export type Parent = import('./types.js').Parent
export type PhrasingContent = import('./types.js').PhrasingContent
export type SafeConfig = import('./types.js').SafeConfig
export type State = import('./types.js').State
export type TrackFields = import('./types.js').TrackFields

View File

@@ -0,0 +1,188 @@
/**
* @typedef {import('./types.js').Enter} Enter
* @typedef {import('./types.js').Info} Info
* @typedef {import('./types.js').Join} Join
* @typedef {import('./types.js').FlowContent} FlowContent
* @typedef {import('./types.js').Node} Node
* @typedef {import('./types.js').Options} Options
* @typedef {import('./types.js').Parent} Parent
* @typedef {import('./types.js').PhrasingContent} PhrasingContent
* @typedef {import('./types.js').SafeConfig} SafeConfig
* @typedef {import('./types.js').State} State
* @typedef {import('./types.js').TrackFields} TrackFields
*/
import {zwitch} from 'zwitch'
import {configure} from './configure.js'
import {handle as handlers} from './handle/index.js'
import {join} from './join.js'
import {unsafe} from './unsafe.js'
import {association} from './util/association.js'
import {containerPhrasing} from './util/container-phrasing.js'
import {containerFlow} from './util/container-flow.js'
import {indentLines} from './util/indent-lines.js'
import {safe} from './util/safe.js'
import {track} from './util/track.js'
/**
* Turn an mdast syntax tree into markdown.
*
* @param {Node} tree
* Tree to serialize.
* @param {Options} [options]
* Configuration (optional).
* @returns {string}
* Serialized markdown representing `tree`.
*/
export function toMarkdown(tree, options = {}) {
/** @type {State} */
const state = {
enter,
indentLines,
associationId: association,
containerPhrasing: containerPhrasingBound,
containerFlow: containerFlowBound,
createTracker: track,
safe: safeBound,
stack: [],
unsafe: [],
join: [],
// @ts-expect-error: well fill it next.
handlers: {},
options: {},
indexStack: [],
// @ts-expect-error: well add `handle` later.
handle: undefined
}
configure(state, {unsafe, join, handlers})
configure(state, options)
if (state.options.tightDefinitions) {
configure(state, {join: [joinDefinition]})
}
state.handle = zwitch('type', {
invalid,
unknown,
handlers: state.handlers
})
let result = state.handle(tree, undefined, state, {
before: '\n',
after: '\n',
now: {line: 1, column: 1},
lineShift: 0
})
if (
result &&
result.charCodeAt(result.length - 1) !== 10 &&
result.charCodeAt(result.length - 1) !== 13
) {
result += '\n'
}
return result
/** @type {Enter} */
function enter(name) {
state.stack.push(name)
return exit
function exit() {
state.stack.pop()
}
}
}
/**
* @param {unknown} value
* @returns {never}
*/
function invalid(value) {
throw new Error('Cannot handle value `' + value + '`, expected node')
}
/**
* @param {unknown} node
* @returns {never}
*/
function unknown(node) {
// @ts-expect-error: fine.
throw new Error('Cannot handle unknown node `' + node.type + '`')
}
/** @type {Join} */
function joinDefinition(left, right) {
// No blank line between adjacent definitions.
if (left.type === 'definition' && left.type === right.type) {
return 0
}
}
/**
* Serialize the children of a parent that contains phrasing children.
*
* These children will be joined flush together.
*
* @this {State}
* Info passed around about the current state.
* @param {Parent & {children: Array<PhrasingContent>}} parent
* Parent of flow nodes.
* @param {Info} info
* Info on where we are in the document we are generating.
* @returns {string}
* Serialized children, joined together.
*/
function containerPhrasingBound(parent, info) {
return containerPhrasing(parent, this, info)
}
/**
* Serialize the children of a parent that contains flow children.
*
* These children will typically be joined by blank lines.
* What they are joined by exactly is defined by `Join` functions.
*
* @this {State}
* Info passed around about the current state.
* @param {Parent & {children: Array<FlowContent>}} parent
* Parent of flow nodes.
* @param {TrackFields} info
* Info on where we are in the document we are generating.
* @returns {string}
* Serialized children, joined by (blank) lines.
*/
function containerFlowBound(parent, info) {
return containerFlow(parent, this, info)
}
/**
* Make a string safe for embedding in markdown constructs.
*
* In markdown, almost all punctuation characters can, in certain cases,
* result in something.
* Whether they do is highly subjective to where they happen and in what
* they happen.
*
* To solve this, `mdast-util-to-markdown` tracks:
*
* * Characters before and after something;
* * What “constructs” we are in.
*
* This information is then used by this function to escape or encode
* special characters.
*
* @this {State}
* Info passed around about the current state.
* @param {string | null | undefined} value
* Raw value to make safe.
* @param {SafeConfig} config
* Configuration.
* @returns {string}
* Serialized markdown safe for embedding.
*/
function safeBound(value, config) {
return safe(this, value, config)
}

View File

@@ -0,0 +1,3 @@
/** @type {Array<Join>} */
export const join: Array<Join>
export type Join = import('./types.js').Join

View File

@@ -0,0 +1,51 @@
/**
* @typedef {import('./types.js').Join} Join
*/
import {formatCodeAsIndented} from './util/format-code-as-indented.js'
import {formatHeadingAsSetext} from './util/format-heading-as-setext.js'
/** @type {Array<Join>} */
export const join = [joinDefaults]
/** @type {Join} */
function joinDefaults(left, right, parent, state) {
// Indented code after list or another indented code.
if (
right.type === 'code' &&
formatCodeAsIndented(right, state) &&
(left.type === 'list' ||
(left.type === right.type && formatCodeAsIndented(left, state)))
) {
return false
}
// Two lists with the same marker.
if (
left.type === 'list' &&
left.type === right.type &&
Boolean(left.ordered) === Boolean(right.ordered) &&
!(left.ordered
? state.options.bulletOrderedOther
: state.options.bulletOther)
) {
return false
}
// Join children of a list or an item.
// In which case, `parent` has a `spread` field.
if ('spread' in parent && typeof parent.spread === 'boolean') {
if (
left.type === 'paragraph' &&
// Two paragraphs.
(left.type === right.type ||
right.type === 'definition' ||
// Paragraph followed by a setext heading.
(right.type === 'heading' && formatHeadingAsSetext(right, state)))
) {
return
}
return parent.spread ? 1 : 0
}
}

View File

@@ -0,0 +1,466 @@
export type UnistParent = import('unist').Parent
export type Point = import('unist').Point
export type Association = import('mdast').Association
export type Content = import('mdast').Content
export type ListContent = import('mdast').ListContent
export type PhrasingContent = import('mdast').PhrasingContent
export type Root = import('mdast').Root
export type TopLevelContent = import('mdast').TopLevelContent
export type ConstructName = import('../index.js').ConstructName
export type Node = Root | Content
export type Parent = Extract<Node, UnistParent>
export type FlowContent = TopLevelContent | ListContent
/**
* Info on where we are in the document we are generating.
*/
export type TrackFields = {
/**
* Current point.
*/
now: Point
/**
* Number of columns each line will be shifted by wrapping nodes.
*/
lineShift: number
}
/**
* Info on the characters that are around the current thing we are
* generating.
*/
export type SafeFields = {
/**
* Characters before this (guaranteed to be one, can be more).
*/
before: string
/**
* Characters after this (guaranteed to be one, can be more).
*/
after: string
}
/**
* Info on the surrounding of the node that is serialized.
*/
export type Info = TrackFields & SafeFields
/**
* Get current tracked info.
*/
export type TrackCurrent = () => TrackFields
/**
* Define a relative increased line shift (the typical indent for lines).
*/
export type TrackShift = (value: number) => void
/**
* Move past some generated markdown.
*/
export type TrackMove = (value: string | null | undefined) => string
/**
* Track positional info in the output.
*
* This info isnt used yet but such functionality will allow line wrapping,
* source maps, etc.
*/
export type Tracker = {
/**
* Get the current tracked info.
*/
current: TrackCurrent
/**
* Define an increased line shift (the typical indent for lines).
*/
shift: TrackShift
/**
* Move past some generated markdown.
*/
move: TrackMove
}
/**
* Track positional info in the output.
*
* This info isnt used yet but such functionality will allow line wrapping,
* source maps, etc.
*/
export type CreateTracker = (info: TrackFields) => Tracker
/**
* Get an identifier from an association to match it to others.
*
* Associations are nodes that match to something else through an ID:
* <https://github.com/syntax-tree/mdast#association>.
*
* The `label` of an association is the string value: character escapes and
* references work, and casing is intact.
* The `identifier` is used to match one association to another:
* controversially, character escapes and references dont work in this
* matching: `&copy;` does not match `©`, and `\+` does not match `+`.
*
* But casing is ignored (and whitespace) is trimmed and collapsed: ` A\nb`
* matches `a b`.
* So, we do prefer the label when figuring out how were going to serialize:
* it has whitespace, casing, and we can ignore most useless character
* escapes and all character references.
*/
export type AssociationId = (node: Association) => string
/**
* Map function to pad a single line.
*/
export type Map = (value: string, line: number, blank: boolean) => string
/**
* Pad serialized markdown.
*/
export type IndentLines = (value: string, map: Map) => string
/**
* Serialize the children of a parent that contains phrasing children.
*
* These children will be joined flush together.
*/
export type ContainerPhrasing = (
parent: Parent & {
children: Array<PhrasingContent>
},
info: Info
) => string
/**
* Serialize the children of a parent that contains flow children.
*
* These children will typically be joined by blank lines.
* What they are joined by exactly is defined by `Join` functions.
*/
export type ContainerFlow = (
parent: Parent & {
children: Array<FlowContent>
},
info: TrackFields
) => string
/**
* Extra configuration for `safe`
*/
export type SafeEncodeFields = {
/**
* Extra characters that *must* be encoded (as character references) instead
* of escaped (character escapes).
*
* Only ASCII punctuation will use character escapes, so you never need to
* pass non-ASCII-punctuation here.
*/
encode?: Array<string> | null | undefined
}
export type SafeConfig = SafeFields & SafeEncodeFields
/**
* Make a string safe for embedding in markdown constructs.
*
* In markdown, almost all punctuation characters can, in certain cases,
* result in something.
* Whether they do is highly subjective to where they happen and in what
* they happen.
*
* To solve this, `mdast-util-to-markdown` tracks:
*
* * Characters before and after something;
* * What “constructs” we are in.
*
* This information is then used by this function to escape or encode
* special characters.
*/
export type Safe = (
input: string | null | undefined,
config: SafeConfig
) => string
/**
* Enter something.
*/
export type Enter = (name: ConstructName) => Exit
/**
* Exit something.
*/
export type Exit = () => void
/**
* Info passed around about the current state.
*/
export type State = {
/**
* Stack of constructs were in.
*/
stack: Array<ConstructName>
/**
* Positions of child nodes in their parents.
*/
indexStack: Array<number>
/**
* Pad serialized markdown.
*/
indentLines: IndentLines
/**
* Get an identifier from an association to match it to others.
*/
associationId: AssociationId
/**
* Serialize the children of a parent that contains phrasing children.
*/
containerPhrasing: ContainerPhrasing
/**
* Serialize the children of a parent that contains flow children.
*/
containerFlow: ContainerFlow
/**
* Track positional info in the output.
*/
createTracker: CreateTracker
/**
* Serialize the children of a parent that contains flow children.
*/
safe: Safe
/**
* Enter a construct (returns a corresponding exit function).
*/
enter: Enter
/**
* Applied user configuration.
*/
options: Options
/**
* Applied unsafe patterns.
*/
unsafe: Array<Unsafe>
/**
* Applied join handlers.
*/
join: Array<Join>
/**
* Call the configured handler for the given node.
*/
handle: Handle
/**
* Applied handlers.
*/
handlers: Handlers
/**
* List marker currently in use.
*/
bulletCurrent: string | undefined
/**
* List marker previously in use.
*/
bulletLastUsed: string | undefined
}
/**
* Handle a particular node.
*/
export type Handle = (
node: any,
parent: Parent | undefined,
state: State,
Info: Info
) => string
/**
* Handle particular nodes.
*
* Each key is a node type, each value its corresponding handler.
*/
export type Handlers = Record<Node['type'], Handle>
/**
* How to join two blocks.
*
* “Blocks” are typically joined by one blank line.
* Sometimes its nicer to have them flush next to each other, yet other
* times they cannot occur together at all.
*
* Join functions receive two adjacent siblings and their parent and what
* they return defines how many blank lines to use between them.
*/
export type Join = (
left: Node,
right: Node,
parent: Parent,
state: State
) => boolean | null | undefined | void | number
/**
* Schema that defines when a character cannot occur.
*/
export type Unsafe = {
/**
* Single unsafe character.
*/
character: string
/**
* Constructs where this is bad.
*/
inConstruct?: ConstructName | Array<ConstructName> | null | undefined
/**
* Constructs where this is fine again.
*/
notInConstruct?: ConstructName | Array<ConstructName> | null | undefined
/**
* `character` is bad when this is before it (cannot be used together with
* `atBreak`).
*/
before?: string | null | undefined
/**
* `character` is bad when this is after it.
*/
after?: string | null | undefined
/**
* `character` is bad at a break (cannot be used together with `before`).
*/
atBreak?: boolean | null | undefined
/**
* The unsafe pattern (this whole object) compiled as a regex.
*
* This is internal and must not be defined.
*/
_compiled?: RegExp | null | undefined
}
/**
* Configuration (optional).
*/
export type Options = {
/**
* Marker to use for bullets of items in unordered lists.
*/
bullet?: '-' | '*' | '+' | null | undefined
/**
* Marker to use in certain cases where the primary bullet doesnt work.
*
* There are three cases where the primary bullet cannot be used:
*
* * When three list items are on their own, the last one is empty, and
* `bullet` is also a valid `rule`: `* - +`.
* This would turn into a thematic break if serialized with three primary
* bullets.
* As this is an edge case unlikely to appear in normal markdown, the
* last list item will be given a different bullet.
* * When a thematic break is the first child of one of the list items, and
* `bullet` is the same character as `rule`: `- ***`.
* This would turn into a single thematic break if serialized with
* primary bullets.
* As this is an edge case unlikely to appear in normal markdown this
* markup is always fixed, even if `bulletOther` is not passed
* * When two unordered lists appear next to each other: `* a\n- b`.
* CommonMark sees different bullets as different lists, but several
* markdown parsers parse it as one list.
* To solve for both, we instead inject an empty comment between the two
* lists: `* a\n<!---->\n* b`, but if `bulletOther` is given explicitly,
* it will be used instead
*/
bulletOther?: '-' | '*' | '+' | null | undefined
/**
* Marker to use for bullets of items in ordered lists.
*/
bulletOrdered?: '.' | ')' | null | undefined
/**
* Marker to use in certain cases where the primary bullet for ordered items
* doesnt work.
*
* There is one case where the primary bullet for ordered items cannot be used:
*
* * When two ordered lists appear next to each other: `1. a\n2) b`.
* CommonMark added support for `)` as a marker, but other markdown
* parsers do not support it.
* To solve for both, we instead inject an empty comment between the two
* lists: `1. a\n<!---->\n1. b`, but if `bulletOrderedOther` is given
* explicitly, it will be used instead
*/
bulletOrderedOther?: '.' | ')' | null | undefined
/**
* Whether to add the same number of number signs (`#`) at the end of an ATX
* heading as the opening sequence.
*/
closeAtx?: boolean | null | undefined
/**
* Marker to use for emphasis.
*/
emphasis?: '_' | '*' | null | undefined
/**
* Marker to use for fenced code.
*/
fence?: '~' | '`' | null | undefined
/**
* Whether to use fenced code always.
*
* The default is to use fenced code if there is a language defined, if the
* code is empty, or if it starts or ends in blank lines.
*/
fences?: boolean | null | undefined
/**
* Whether to increment the counter of ordered lists items.
*/
incrementListMarker?: boolean | null | undefined
/**
* How to indent the content of list items.
*
* Either with the size of the bullet plus one space (when `'one'`), a tab
* stop (`'tab'`), or depending on the item and its parent list (`'mixed'`,
* uses `'one'` if the item and list are tight and `'tab'` otherwise).
*/
listItemIndent?: 'tab' | 'one' | 'mixed' | null | undefined
/**
* Marker to use for titles.
*/
quote?: '"' | "'" | null | undefined
/**
* Whether to always use resource links.
*
* The default is to use autolinks (`<https://example.com>`) when possible
* and resource links (`[text](url)`) otherwise.
*/
resourceLink?: boolean | null | undefined
/**
* Marker to use for thematic breaks.
*/
rule?: '-' | '_' | '*' | null | undefined
/**
* Number of markers to use for thematic breaks.
*/
ruleRepetition?: number | null | undefined
/**
* Whether to add spaces between markers in thematic breaks.
*/
ruleSpaces?: boolean | null | undefined
/**
* Whether to use setext headings when possible.
*
* The default is to always use ATX headings (`# heading`) instead of setext
* headings (`heading\n=======`).
* Setext headings cannot be used for empty headings or headings with a rank
* of three or more.
*/
setext?: boolean | null | undefined
/**
* Marker to use for strong.
*/
strong?: '_' | '*' | null | undefined
/**
* Whether to join definitions without a blank line.
*
* The default is to add blank lines between any flow (“block”) construct.
* Turning this option on is a shortcut for a join function like so:
*
* ```js
* function joinTightDefinitions(left, right) {
* if (left.type === 'definition' && right.type === 'definition') {
* return 0
* }
* }
* ```
*/
tightDefinitions?: boolean | null | undefined
/**
* Handle particular nodes.
*
* Each key is a node type, each value its corresponding handler.
*/
handlers?: Partial<Handlers> | null | undefined
/**
* How to join blocks.
*/
join?: Array<Join> | null | undefined
/**
* Schemas that define when characters cannot occur.
*/
unsafe?: Array<Unsafe> | null | undefined
/**
* List of extensions to include.
*
* Each `ToMarkdownExtension` is an object with the same interface as
* `Options` here.
*/
extensions?: Array<Options> | null | undefined
}

View File

@@ -0,0 +1,408 @@
/**
* @typedef {import('unist').Parent} UnistParent
* @typedef {import('unist').Point} Point
* @typedef {import('mdast').Association} Association
* @typedef {import('mdast').Content} Content
* @typedef {import('mdast').ListContent} ListContent
* @typedef {import('mdast').PhrasingContent} PhrasingContent
* @typedef {import('mdast').Root} Root
* @typedef {import('mdast').TopLevelContent} TopLevelContent
* @typedef {import('../index.js').ConstructName} ConstructName
*/
/**
* @typedef {Root | Content} Node
* @typedef {Extract<Node, UnistParent>} Parent
* @typedef {TopLevelContent | ListContent} FlowContent
*
* @typedef TrackFields
* Info on where we are in the document we are generating.
* @property {Point} now
* Current point.
* @property {number} lineShift
* Number of columns each line will be shifted by wrapping nodes.
*
* @typedef SafeFields
* Info on the characters that are around the current thing we are
* generating.
* @property {string} before
* Characters before this (guaranteed to be one, can be more).
* @property {string} after
* Characters after this (guaranteed to be one, can be more).
*
* @typedef {TrackFields & SafeFields} Info
* Info on the surrounding of the node that is serialized.
*
* @callback TrackCurrent
* Get current tracked info.
* @returns {TrackFields}
* Current tracked info.
*
* @callback TrackShift
* Define a relative increased line shift (the typical indent for lines).
* @param {number} value
* Relative increment in how much each line will be padded.
* @returns {void}
* Nothing.
*
* @callback TrackMove
* Move past some generated markdown.
* @param {string | null | undefined} value
* Generated markdown.
* @returns {string}
* Given markdown.
*
* @typedef Tracker
* Track positional info in the output.
*
* This info isnt used yet but such functionality will allow line wrapping,
* source maps, etc.
* @property {TrackCurrent} current
* Get the current tracked info.
* @property {TrackShift} shift
* Define an increased line shift (the typical indent for lines).
* @property {TrackMove} move
* Move past some generated markdown.
*
* @callback CreateTracker
* Track positional info in the output.
*
* This info isnt used yet but such functionality will allow line wrapping,
* source maps, etc.
* @param {TrackFields} info
* Info on where we are in the document we are generating.
* @returns {Tracker}
* Tracker.
*
* @callback AssociationId
* Get an identifier from an association to match it to others.
*
* Associations are nodes that match to something else through an ID:
* <https://github.com/syntax-tree/mdast#association>.
*
* The `label` of an association is the string value: character escapes and
* references work, and casing is intact.
* The `identifier` is used to match one association to another:
* controversially, character escapes and references dont work in this
* matching: `&copy;` does not match `©`, and `\+` does not match `+`.
*
* But casing is ignored (and whitespace) is trimmed and collapsed: ` A\nb`
* matches `a b`.
* So, we do prefer the label when figuring out how were going to serialize:
* it has whitespace, casing, and we can ignore most useless character
* escapes and all character references.
* @param {Association} node
* Node that includes an association.
* @returns {string}
* ID.
*
* @callback Map
* Map function to pad a single line.
* @param {string} value
* A single line of serialized markdown.
* @param {number} line
* Line number relative to the fragment.
* @param {boolean} blank
* Whether the line is considered blank in markdown.
* @returns {string}
* Padded line.
*
* @callback IndentLines
* Pad serialized markdown.
* @param {string} value
* Whole fragment of serialized markdown.
* @param {Map} map
* Map function.
* @returns {string}
* Padded value.
*
* @callback ContainerPhrasing
* Serialize the children of a parent that contains phrasing children.
*
* These children will be joined flush together.
* @param {Parent & {children: Array<PhrasingContent>}} parent
* Parent of flow nodes.
* @param {Info} info
* Info on where we are in the document we are generating.
* @returns {string}
* Serialized children, joined together.
*
* @callback ContainerFlow
* Serialize the children of a parent that contains flow children.
*
* These children will typically be joined by blank lines.
* What they are joined by exactly is defined by `Join` functions.
* @param {Parent & {children: Array<FlowContent>}} parent
* Parent of flow nodes.
* @param {TrackFields} info
* Info on where we are in the document we are generating.
* @returns {string}
* Serialized children, joined by (blank) lines.
*
* @typedef SafeEncodeFields
* Extra configuration for `safe`
* @property {Array<string> | null | undefined} [encode]
* Extra characters that *must* be encoded (as character references) instead
* of escaped (character escapes).
*
* Only ASCII punctuation will use character escapes, so you never need to
* pass non-ASCII-punctuation here.
*
* @typedef {SafeFields & SafeEncodeFields} SafeConfig
*
* @callback Safe
* Make a string safe for embedding in markdown constructs.
*
* In markdown, almost all punctuation characters can, in certain cases,
* result in something.
* Whether they do is highly subjective to where they happen and in what
* they happen.
*
* To solve this, `mdast-util-to-markdown` tracks:
*
* * Characters before and after something;
* * What “constructs” we are in.
*
* This information is then used by this function to escape or encode
* special characters.
* @param {string | null | undefined} input
* Raw value to make safe.
* @param {SafeConfig} config
* Configuration.
* @returns {string}
* Serialized markdown safe for embedding.
*
* @callback Enter
* Enter something.
* @param {ConstructName} name
* Label, more similar to a micromark event than an mdast node type.
* @returns {Exit}
* Revert.
*
* @callback Exit
* Exit something.
* @returns {void}
* Nothing.
*
* @typedef State
* Info passed around about the current state.
* @property {Array<ConstructName>} stack
* Stack of constructs were in.
* @property {Array<number>} indexStack
* Positions of child nodes in their parents.
* @property {IndentLines} indentLines
* Pad serialized markdown.
* @property {AssociationId} associationId
* Get an identifier from an association to match it to others.
* @property {ContainerPhrasing} containerPhrasing
* Serialize the children of a parent that contains phrasing children.
* @property {ContainerFlow} containerFlow
* Serialize the children of a parent that contains flow children.
* @property {CreateTracker} createTracker
* Track positional info in the output.
* @property {Safe} safe
* Serialize the children of a parent that contains flow children.
* @property {Enter} enter
* Enter a construct (returns a corresponding exit function).
* @property {Options} options
* Applied user configuration.
* @property {Array<Unsafe>} unsafe
* Applied unsafe patterns.
* @property {Array<Join>} join
* Applied join handlers.
* @property {Handle} handle
* Call the configured handler for the given node.
* @property {Handlers} handlers
* Applied handlers.
* @property {string | undefined} bulletCurrent
* List marker currently in use.
* @property {string | undefined} bulletLastUsed
* List marker previously in use.
*
* @callback Handle
* Handle a particular node.
* @param {any} node
* Expected mdast node.
* @param {Parent | undefined} parent
* Parent of `node`.
* @param {State} state
* Info passed around about the current state.
* @param {Info} Info
* Info on the surrounding of the node that is serialized.
* @returns {string}
* Serialized markdown representing `node`.
*
* @typedef {Record<Node['type'], Handle>} Handlers
* Handle particular nodes.
*
* Each key is a node type, each value its corresponding handler.
*
* @callback Join
* How to join two blocks.
*
* “Blocks” are typically joined by one blank line.
* Sometimes its nicer to have them flush next to each other, yet other
* times they cannot occur together at all.
*
* Join functions receive two adjacent siblings and their parent and what
* they return defines how many blank lines to use between them.
* @param {Node} left
* First of two adjacent siblings.
* @param {Node} right
* Second of two adjacent siblings.
* @param {Parent} parent
* Parent of the two siblings.
* @param {State} state
* Info passed around about the current state.
* @returns {boolean | null | undefined | void | number}
* How many blank lines to use between the siblings.
*
* Where `true` is as passing `1` and `false` means the nodes cannot be
* joined by a blank line, such as two adjacent block quotes or indented code
* after a list, in which case a comment will be injected to break them up:
*
* ```markdown
* > Quote 1
*
* <!---->
*
* > Quote 2
* ```
*
* > 👉 **Note**: abusing this feature will break markdown.
* > One such example is when returning `0` for two paragraphs, which will
* > result in the text running together, and in the future to be seen as
* > one paragraph.
*
* @typedef Unsafe
* Schema that defines when a character cannot occur.
* @property {string} character
* Single unsafe character.
* @property {ConstructName | Array<ConstructName> | null | undefined} [inConstruct]
* Constructs where this is bad.
* @property {ConstructName | Array<ConstructName> | null | undefined} [notInConstruct]
* Constructs where this is fine again.
* @property {string | null | undefined} [before]
* `character` is bad when this is before it (cannot be used together with
* `atBreak`).
* @property {string | null | undefined} [after]
* `character` is bad when this is after it.
* @property {boolean | null | undefined} [atBreak]
* `character` is bad at a break (cannot be used together with `before`).
* @property {RegExp | null | undefined} [_compiled]
* The unsafe pattern (this whole object) compiled as a regex.
*
* This is internal and must not be defined.
*
* @typedef Options
* Configuration (optional).
* @property {'-' | '*' | '+' | null | undefined} [bullet='*']
* Marker to use for bullets of items in unordered lists.
* @property {'-' | '*' | '+' | null | undefined} [bulletOther]
* Marker to use in certain cases where the primary bullet doesnt work.
*
* There are three cases where the primary bullet cannot be used:
*
* * When three list items are on their own, the last one is empty, and
* `bullet` is also a valid `rule`: `* - +`.
* This would turn into a thematic break if serialized with three primary
* bullets.
* As this is an edge case unlikely to appear in normal markdown, the
* last list item will be given a different bullet.
* * When a thematic break is the first child of one of the list items, and
* `bullet` is the same character as `rule`: `- ***`.
* This would turn into a single thematic break if serialized with
* primary bullets.
* As this is an edge case unlikely to appear in normal markdown this
* markup is always fixed, even if `bulletOther` is not passed
* * When two unordered lists appear next to each other: `* a\n- b`.
* CommonMark sees different bullets as different lists, but several
* markdown parsers parse it as one list.
* To solve for both, we instead inject an empty comment between the two
* lists: `* a\n<!---->\n* b`, but if `bulletOther` is given explicitly,
* it will be used instead
* @property {'.' | ')' | null | undefined} [bulletOrdered='.']
* Marker to use for bullets of items in ordered lists.
* @property {'.' | ')' | null | undefined} [bulletOrderedOther]
* Marker to use in certain cases where the primary bullet for ordered items
* doesnt work.
*
* There is one case where the primary bullet for ordered items cannot be used:
*
* * When two ordered lists appear next to each other: `1. a\n2) b`.
* CommonMark added support for `)` as a marker, but other markdown
* parsers do not support it.
* To solve for both, we instead inject an empty comment between the two
* lists: `1. a\n<!---->\n1. b`, but if `bulletOrderedOther` is given
* explicitly, it will be used instead
* @property {boolean | null | undefined} [closeAtx=false]
* Whether to add the same number of number signs (`#`) at the end of an ATX
* heading as the opening sequence.
* @property {'_' | '*' | null | undefined} [emphasis='*']
* Marker to use for emphasis.
* @property {'~' | '`' | null | undefined} [fence='`']
* Marker to use for fenced code.
* @property {boolean | null | undefined} [fences=false]
* Whether to use fenced code always.
*
* The default is to use fenced code if there is a language defined, if the
* code is empty, or if it starts or ends in blank lines.
* @property {boolean | null | undefined} [incrementListMarker=true]
* Whether to increment the counter of ordered lists items.
* @property {'tab' | 'one' | 'mixed' | null | undefined} [listItemIndent='tab']
* How to indent the content of list items.
*
* Either with the size of the bullet plus one space (when `'one'`), a tab
* stop (`'tab'`), or depending on the item and its parent list (`'mixed'`,
* uses `'one'` if the item and list are tight and `'tab'` otherwise).
* @property {'"' | "'" | null | undefined} [quote='"']
* Marker to use for titles.
* @property {boolean | null | undefined} [resourceLink=false]
* Whether to always use resource links.
*
* The default is to use autolinks (`<https://example.com>`) when possible
* and resource links (`[text](url)`) otherwise.
* @property {'-' | '_' | '*' | null | undefined} [rule='*']
* Marker to use for thematic breaks.
* @property {number | null | undefined} [ruleRepetition=3]
* Number of markers to use for thematic breaks.
* @property {boolean | null | undefined} [ruleSpaces=false]
* Whether to add spaces between markers in thematic breaks.
* @property {boolean | null | undefined} [setext=false]
* Whether to use setext headings when possible.
*
* The default is to always use ATX headings (`# heading`) instead of setext
* headings (`heading\n=======`).
* Setext headings cannot be used for empty headings or headings with a rank
* of three or more.
* @property {'_' | '*' | null | undefined} [strong='*']
* Marker to use for strong.
* @property {boolean | null | undefined} [tightDefinitions=false]
* Whether to join definitions without a blank line.
*
* The default is to add blank lines between any flow (“block”) construct.
* Turning this option on is a shortcut for a join function like so:
*
* ```js
* function joinTightDefinitions(left, right) {
* if (left.type === 'definition' && right.type === 'definition') {
* return 0
* }
* }
* ```
* @property {Partial<Handlers> | null | undefined} [handlers={}]
* Handle particular nodes.
*
* Each key is a node type, each value its corresponding handler.
* @property {Array<Join> | null | undefined} [join=[]]
* How to join blocks.
* @property {Array<Unsafe> | null | undefined} [unsafe=[]]
* Schemas that define when characters cannot occur.
* @property {Array<Options> | null | undefined} [extensions=[]]
* List of extensions to include.
*
* Each `ToMarkdownExtension` is an object with the same interface as
* `Options` here.
*/
export {}

View File

@@ -0,0 +1,4 @@
/** @type {Array<Unsafe>} */
export const unsafe: Array<Unsafe>
export type Unsafe = import('./types.js').Unsafe
export type ConstructName = import('./types.js').ConstructName

View File

@@ -0,0 +1,147 @@
/**
* @typedef {import('./types.js').Unsafe} Unsafe
* @typedef {import('./types.js').ConstructName} ConstructName
*/
/**
* List of constructs that occur in phrasing (paragraphs, headings), but cannot
* contain things like attention (emphasis, strong), images, or links.
* So they sort of cancel each other out.
* Note: could use a better name.
*
* @type {Array<ConstructName>}
*/
const fullPhrasingSpans = [
'autolink',
'destinationLiteral',
'destinationRaw',
'reference',
'titleQuote',
'titleApostrophe'
]
/** @type {Array<Unsafe>} */
export const unsafe = [
{character: '\t', after: '[\\r\\n]', inConstruct: 'phrasing'},
{character: '\t', before: '[\\r\\n]', inConstruct: 'phrasing'},
{
character: '\t',
inConstruct: ['codeFencedLangGraveAccent', 'codeFencedLangTilde']
},
{
character: '\r',
inConstruct: [
'codeFencedLangGraveAccent',
'codeFencedLangTilde',
'codeFencedMetaGraveAccent',
'codeFencedMetaTilde',
'destinationLiteral',
'headingAtx'
]
},
{
character: '\n',
inConstruct: [
'codeFencedLangGraveAccent',
'codeFencedLangTilde',
'codeFencedMetaGraveAccent',
'codeFencedMetaTilde',
'destinationLiteral',
'headingAtx'
]
},
{character: ' ', after: '[\\r\\n]', inConstruct: 'phrasing'},
{character: ' ', before: '[\\r\\n]', inConstruct: 'phrasing'},
{
character: ' ',
inConstruct: ['codeFencedLangGraveAccent', 'codeFencedLangTilde']
},
// An exclamation mark can start an image, if it is followed by a link or
// a link reference.
{
character: '!',
after: '\\[',
inConstruct: 'phrasing',
notInConstruct: fullPhrasingSpans
},
// A quote can break out of a title.
{character: '"', inConstruct: 'titleQuote'},
// A number sign could start an ATX heading if it starts a line.
{atBreak: true, character: '#'},
{character: '#', inConstruct: 'headingAtx', after: '(?:[\r\n]|$)'},
// Dollar sign and percentage are not used in markdown.
// An ampersand could start a character reference.
{character: '&', after: '[#A-Za-z]', inConstruct: 'phrasing'},
// An apostrophe can break out of a title.
{character: "'", inConstruct: 'titleApostrophe'},
// A left paren could break out of a destination raw.
{character: '(', inConstruct: 'destinationRaw'},
// A left paren followed by `]` could make something into a link or image.
{
before: '\\]',
character: '(',
inConstruct: 'phrasing',
notInConstruct: fullPhrasingSpans
},
// A right paren could start a list item or break out of a destination
// raw.
{atBreak: true, before: '\\d+', character: ')'},
{character: ')', inConstruct: 'destinationRaw'},
// An asterisk can start thematic breaks, list items, emphasis, strong.
{atBreak: true, character: '*', after: '(?:[ \t\r\n*])'},
{character: '*', inConstruct: 'phrasing', notInConstruct: fullPhrasingSpans},
// A plus sign could start a list item.
{atBreak: true, character: '+', after: '(?:[ \t\r\n])'},
// A dash can start thematic breaks, list items, and setext heading
// underlines.
{atBreak: true, character: '-', after: '(?:[ \t\r\n-])'},
// A dot could start a list item.
{atBreak: true, before: '\\d+', character: '.', after: '(?:[ \t\r\n]|$)'},
// Slash, colon, and semicolon are not used in markdown for constructs.
// A less than can start html (flow or text) or an autolink.
// HTML could start with an exclamation mark (declaration, cdata, comment),
// slash (closing tag), question mark (instruction), or a letter (tag).
// An autolink also starts with a letter.
// Finally, it could break out of a destination literal.
{atBreak: true, character: '<', after: '[!/?A-Za-z]'},
{
character: '<',
after: '[!/?A-Za-z]',
inConstruct: 'phrasing',
notInConstruct: fullPhrasingSpans
},
{character: '<', inConstruct: 'destinationLiteral'},
// An equals to can start setext heading underlines.
{atBreak: true, character: '='},
// A greater than can start block quotes and it can break out of a
// destination literal.
{atBreak: true, character: '>'},
{character: '>', inConstruct: 'destinationLiteral'},
// Question mark and at sign are not used in markdown for constructs.
// A left bracket can start definitions, references, labels,
{atBreak: true, character: '['},
{character: '[', inConstruct: 'phrasing', notInConstruct: fullPhrasingSpans},
{character: '[', inConstruct: ['label', 'reference']},
// A backslash can start an escape (when followed by punctuation) or a
// hard break (when followed by an eol).
// Note: typical escapes are handled in `safe`!
{character: '\\', after: '[\\r\\n]', inConstruct: 'phrasing'},
// A right bracket can exit labels.
{character: ']', inConstruct: ['label', 'reference']},
// Caret is not used in markdown for constructs.
// An underscore can start emphasis, strong, or a thematic break.
{atBreak: true, character: '_'},
{character: '_', inConstruct: 'phrasing', notInConstruct: fullPhrasingSpans},
// A grave accent can start code (fenced or text), or it can break out of
// a grave accent code fence.
{atBreak: true, character: '`'},
{
character: '`',
inConstruct: ['codeFencedLangGraveAccent', 'codeFencedMetaGraveAccent']
},
{character: '`', inConstruct: 'phrasing', notInConstruct: fullPhrasingSpans},
// Left brace, vertical bar, right brace are not used in markdown for
// constructs.
// A tilde can start code (fenced).
{atBreak: true, character: '~'}
]

View File

@@ -0,0 +1,2 @@
export function association(node: import('mdast').Association): string
export type AssociationId = import('../types.js').AssociationId

View File

@@ -0,0 +1,33 @@
/**
* @typedef {import('../types.js').AssociationId} AssociationId
*/
import {decodeString} from 'micromark-util-decode-string'
/**
* Get an identifier from an association to match it to others.
*
* Associations are nodes that match to something else through an ID:
* <https://github.com/syntax-tree/mdast#association>.
*
* The `label` of an association is the string value: character escapes and
* references work, and casing is intact.
* The `identifier` is used to match one association to another:
* controversially, character escapes and references dont work in this
* matching: `&copy;` does not match `©`, and `\+` does not match `+`.
*
* But casing is ignored (and whitespace) is trimmed and collapsed: ` A\nb`
* matches `a b`.
* So, we do prefer the label when figuring out how were going to serialize:
* it has whitespace, casing, and we can ignore most useless character
* escapes and all character references.
*
* @type {AssociationId}
*/
export function association(node) {
if (node.label || !node.identifier) {
return node.label || ''
}
return decodeString(node.identifier)
}

View File

@@ -0,0 +1,9 @@
/**
* @param {State} state
* @returns {Exclude<Options['bulletOrdered'], null | undefined>}
*/
export function checkBulletOrderedOther(
state: State
): Exclude<Options['bulletOrdered'], null | undefined>
export type State = import('../types.js').State
export type Options = import('../types.js').Options

View File

@@ -0,0 +1,39 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
import {checkBulletOrdered} from './check-bullet-ordered.js'
/**
* @param {State} state
* @returns {Exclude<Options['bulletOrdered'], null | undefined>}
*/
export function checkBulletOrderedOther(state) {
const bulletOrdered = checkBulletOrdered(state)
const bulletOrderedOther = state.options.bulletOrderedOther
if (!bulletOrderedOther) {
return bulletOrdered === '.' ? ')' : '.'
}
if (bulletOrderedOther !== '.' && bulletOrderedOther !== ')') {
throw new Error(
'Cannot serialize items with `' +
bulletOrderedOther +
'` for `options.bulletOrderedOther`, expected `*`, `+`, or `-`'
)
}
if (bulletOrderedOther === bulletOrdered) {
throw new Error(
'Expected `bulletOrdered` (`' +
bulletOrdered +
'`) and `bulletOrderedOther` (`' +
bulletOrderedOther +
'`) to be different'
)
}
return bulletOrderedOther
}

View File

@@ -0,0 +1,13 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['bulletOrdered'], null | undefined>}
*/
export function checkBulletOrdered(
state: State
): Exclude<Options['bulletOrdered'], null | undefined>
export type State = import('../types.js').State
export type Options = import('../types.js').Options

View File

@@ -0,0 +1,22 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['bulletOrdered'], null | undefined>}
*/
export function checkBulletOrdered(state) {
const marker = state.options.bulletOrdered || '.'
if (marker !== '.' && marker !== ')') {
throw new Error(
'Cannot serialize items with `' +
marker +
'` for `options.bulletOrdered`, expected `.` or `)`'
)
}
return marker
}

View File

@@ -0,0 +1,9 @@
/**
* @param {State} state
* @returns {Exclude<Options['bullet'], null | undefined>}
*/
export function checkBulletOther(
state: State
): Exclude<Options['bullet'], null | undefined>
export type State = import('../types.js').State
export type Options = import('../types.js').Options

View File

@@ -0,0 +1,39 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
import {checkBullet} from './check-bullet.js'
/**
* @param {State} state
* @returns {Exclude<Options['bullet'], null | undefined>}
*/
export function checkBulletOther(state) {
const bullet = checkBullet(state)
const bulletOther = state.options.bulletOther
if (!bulletOther) {
return bullet === '*' ? '-' : '*'
}
if (bulletOther !== '*' && bulletOther !== '+' && bulletOther !== '-') {
throw new Error(
'Cannot serialize items with `' +
bulletOther +
'` for `options.bulletOther`, expected `*`, `+`, or `-`'
)
}
if (bulletOther === bullet) {
throw new Error(
'Expected `bullet` (`' +
bullet +
'`) and `bulletOther` (`' +
bulletOther +
'`) to be different'
)
}
return bulletOther
}

View File

@@ -0,0 +1,13 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['bullet'], null | undefined>}
*/
export function checkBullet(
state: State
): Exclude<Options['bullet'], null | undefined>
export type State = import('../types.js').State
export type Options = import('../types.js').Options

View File

@@ -0,0 +1,22 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['bullet'], null | undefined>}
*/
export function checkBullet(state) {
const marker = state.options.bullet || '*'
if (marker !== '*' && marker !== '+' && marker !== '-') {
throw new Error(
'Cannot serialize items with `' +
marker +
'` for `options.bullet`, expected `*`, `+`, or `-`'
)
}
return marker
}

View File

@@ -0,0 +1,13 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['emphasis'], null | undefined>}
*/
export function checkEmphasis(
state: State
): Exclude<Options['emphasis'], null | undefined>
export type State = import('../types.js').State
export type Options = import('../types.js').Options

View File

@@ -0,0 +1,22 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['emphasis'], null | undefined>}
*/
export function checkEmphasis(state) {
const marker = state.options.emphasis || '*'
if (marker !== '*' && marker !== '_') {
throw new Error(
'Cannot serialize emphasis with `' +
marker +
'` for `options.emphasis`, expected `*`, or `_`'
)
}
return marker
}

View File

@@ -0,0 +1,13 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['fence'], null | undefined>}
*/
export function checkFence(
state: State
): Exclude<Options['fence'], null | undefined>
export type State = import('../types.js').State
export type Options = import('../types.js').Options

View File

@@ -0,0 +1,22 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['fence'], null | undefined>}
*/
export function checkFence(state) {
const marker = state.options.fence || '`'
if (marker !== '`' && marker !== '~') {
throw new Error(
'Cannot serialize code with `' +
marker +
'` for `options.fence`, expected `` ` `` or `~`'
)
}
return marker
}

View File

@@ -0,0 +1,13 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['listItemIndent'], null | undefined>}
*/
export function checkListItemIndent(
state: State
): Exclude<Options['listItemIndent'], null | undefined>
export type State = import('../types.js').State
export type Options = import('../types.js').Options

View File

@@ -0,0 +1,28 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['listItemIndent'], null | undefined>}
*/
export function checkListItemIndent(state) {
const style = state.options.listItemIndent || 'tab'
// To do: remove in a major.
// @ts-expect-error: deprecated.
if (style === 1 || style === '1') {
return 'one'
}
if (style !== 'tab' && style !== 'one' && style !== 'mixed') {
throw new Error(
'Cannot serialize items with `' +
style +
'` for `options.listItemIndent`, expected `tab`, `one`, or `mixed`'
)
}
return style
}

View File

@@ -0,0 +1,13 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['quote'], null | undefined>}
*/
export function checkQuote(
state: State
): Exclude<Options['quote'], null | undefined>
export type State = import('../types.js').State
export type Options = import('../types.js').Options

View File

@@ -0,0 +1,22 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['quote'], null | undefined>}
*/
export function checkQuote(state) {
const marker = state.options.quote || '"'
if (marker !== '"' && marker !== "'") {
throw new Error(
'Cannot serialize title with `' +
marker +
'` for `options.quote`, expected `"`, or `\'`'
)
}
return marker
}

View File

@@ -0,0 +1,13 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['ruleRepetition'], null | undefined>}
*/
export function checkRuleRepetition(
state: State
): Exclude<Options['ruleRepetition'], null | undefined>
export type State = import('../types.js').State
export type Options = import('../types.js').Options

View File

@@ -0,0 +1,22 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['ruleRepetition'], null | undefined>}
*/
export function checkRuleRepetition(state) {
const repetition = state.options.ruleRepetition || 3
if (repetition < 3) {
throw new Error(
'Cannot serialize rules with repetition `' +
repetition +
'` for `options.ruleRepetition`, expected `3` or more'
)
}
return repetition
}

View File

@@ -0,0 +1,13 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['rule'], null | undefined>}
*/
export function checkRule(
state: State
): Exclude<Options['rule'], null | undefined>
export type State = import('../types.js').State
export type Options = import('../types.js').Options

View File

@@ -0,0 +1,22 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['rule'], null | undefined>}
*/
export function checkRule(state) {
const marker = state.options.rule || '*'
if (marker !== '*' && marker !== '-' && marker !== '_') {
throw new Error(
'Cannot serialize rules with `' +
marker +
'` for `options.rule`, expected `*`, `-`, or `_`'
)
}
return marker
}

View File

@@ -0,0 +1,13 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['strong'], null | undefined>}
*/
export function checkStrong(
state: State
): Exclude<Options['strong'], null | undefined>
export type State = import('../types.js').State
export type Options = import('../types.js').Options

View File

@@ -0,0 +1,22 @@
/**
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').Options} Options
*/
/**
* @param {State} state
* @returns {Exclude<Options['strong'], null | undefined>}
*/
export function checkStrong(state) {
const marker = state.options.strong || '*'
if (marker !== '*' && marker !== '_') {
throw new Error(
'Cannot serialize strong with `' +
marker +
'` for `options.strong`, expected `*`, or `_`'
)
}
return marker
}

View File

@@ -0,0 +1,29 @@
/**
* @typedef {import('../types.js').FlowContent} FlowContent
* @typedef {import('../types.js').Node} Node
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').TrackFields} TrackFields
*/
/**
* @param {Parent & {children: Array<FlowContent>}} parent
* Parent of flow nodes.
* @param {State} state
* Info passed around about the current state.
* @param {TrackFields} info
* Info on where we are in the document we are generating.
* @returns {string}
* Serialized children, joined by (blank) lines.
*/
export function containerFlow(
parent: import('../types.js').Parent & {
children: Array<FlowContent>
},
state: State,
info: TrackFields
): string
export type FlowContent = import('../types.js').FlowContent
export type Node = import('../types.js').Node
export type Parent = import('../types.js').Parent
export type State = import('../types.js').State
export type TrackFields = import('../types.js').TrackFields

View File

@@ -0,0 +1,87 @@
/**
* @typedef {import('../types.js').FlowContent} FlowContent
* @typedef {import('../types.js').Node} Node
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').State} State
* @typedef {import('../types.js').TrackFields} TrackFields
*/
/**
* @param {Parent & {children: Array<FlowContent>}} parent
* Parent of flow nodes.
* @param {State} state
* Info passed around about the current state.
* @param {TrackFields} info
* Info on where we are in the document we are generating.
* @returns {string}
* Serialized children, joined by (blank) lines.
*/
export function containerFlow(parent, state, info) {
const indexStack = state.indexStack
const children = parent.children || []
const tracker = state.createTracker(info)
/** @type {Array<string>} */
const results = []
let index = -1
indexStack.push(-1)
while (++index < children.length) {
const child = children[index]
indexStack[indexStack.length - 1] = index
results.push(
tracker.move(
state.handle(child, parent, state, {
before: '\n',
after: '\n',
...tracker.current()
})
)
)
if (child.type !== 'list') {
state.bulletLastUsed = undefined
}
if (index < children.length - 1) {
results.push(
tracker.move(between(child, children[index + 1], parent, state))
)
}
}
indexStack.pop()
return results.join('')
}
/**
* @param {Node} left
* @param {Node} right
* @param {Parent} parent
* @param {State} state
* @returns {string}
*/
function between(left, right, parent, state) {
let index = state.join.length
while (index--) {
const result = state.join[index](left, right, parent, state)
if (result === true || result === 1) {
break
}
if (typeof result === 'number') {
return '\n'.repeat(1 + result)
}
if (result === false) {
return '\n\n<!---->\n\n'
}
}
return '\n\n'
}

View File

@@ -0,0 +1,33 @@
/**
* @typedef {import('../types.js').Handle} Handle
* @typedef {import('../types.js').Info} Info
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').PhrasingContent} PhrasingContent
* @typedef {import('../types.js').State} State
*/
/**
* Serialize the children of a parent that contains phrasing children.
*
* These children will be joined flush together.
*
* @param {Parent & {children: Array<PhrasingContent>}} parent
* Parent of flow nodes.
* @param {State} state
* Info passed around about the current state.
* @param {Info} info
* Info on where we are in the document we are generating.
* @returns {string}
* Serialized children, joined together.
*/
export function containerPhrasing(
parent: import('../types.js').Parent & {
children: Array<PhrasingContent>
},
state: State,
info: Info
): string
export type Handle = import('../types.js').Handle
export type Info = import('../types.js').Info
export type Parent = import('../types.js').Parent
export type PhrasingContent = import('../types.js').PhrasingContent
export type State = import('../types.js').State

View File

@@ -0,0 +1,97 @@
/**
* @typedef {import('../types.js').Handle} Handle
* @typedef {import('../types.js').Info} Info
* @typedef {import('../types.js').Parent} Parent
* @typedef {import('../types.js').PhrasingContent} PhrasingContent
* @typedef {import('../types.js').State} State
*/
/**
* Serialize the children of a parent that contains phrasing children.
*
* These children will be joined flush together.
*
* @param {Parent & {children: Array<PhrasingContent>}} parent
* Parent of flow nodes.
* @param {State} state
* Info passed around about the current state.
* @param {Info} info
* Info on where we are in the document we are generating.
* @returns {string}
* Serialized children, joined together.
*/
export function containerPhrasing(parent, state, info) {
const indexStack = state.indexStack
const children = parent.children || []
/** @type {Array<string>} */
const results = []
let index = -1
let before = info.before
indexStack.push(-1)
let tracker = state.createTracker(info)
while (++index < children.length) {
const child = children[index]
/** @type {string} */
let after
indexStack[indexStack.length - 1] = index
if (index + 1 < children.length) {
/** @type {Handle} */
// @ts-expect-error: hush, its actually a `zwitch`.
let handle = state.handle.handlers[children[index + 1].type]
/** @type {Handle} */
// @ts-expect-error: hush, its actually a `zwitch`.
if (handle && handle.peek) handle = handle.peek
after = handle
? handle(children[index + 1], parent, state, {
before: '',
after: '',
...tracker.current()
}).charAt(0)
: ''
} else {
after = info.after
}
// In some cases, html (text) can be found in phrasing right after an eol.
// When wed serialize that, in most cases that would be seen as html
// (flow).
// As we cant escape or so to prevent it from happening, we take a somewhat
// reasonable approach: replace that eol with a space.
// See: <https://github.com/syntax-tree/mdast-util-to-markdown/issues/15>
if (
results.length > 0 &&
(before === '\r' || before === '\n') &&
child.type === 'html'
) {
results[results.length - 1] = results[results.length - 1].replace(
/(\r?\n|\r)$/,
' '
)
before = ' '
// To do: does this work to reset tracker?
tracker = state.createTracker(info)
tracker.move(results.join(''))
}
results.push(
tracker.move(
state.handle(child, parent, state, {
...tracker.current(),
before,
after
})
)
)
before = results[results.length - 1].slice(-1)
}
indexStack.pop()
return results.join('')
}

View File

@@ -0,0 +1,12 @@
/**
* @typedef {import('mdast').Code} Code
* @typedef {import('../types.js').State} State
*/
/**
* @param {Code} node
* @param {State} state
* @returns {boolean}
*/
export function formatCodeAsIndented(node: Code, state: State): boolean
export type Code = import('mdast').Code
export type State = import('../types.js').State

View File

@@ -0,0 +1,22 @@
/**
* @typedef {import('mdast').Code} Code
* @typedef {import('../types.js').State} State
*/
/**
* @param {Code} node
* @param {State} state
* @returns {boolean}
*/
export function formatCodeAsIndented(node, state) {
return Boolean(
!state.options.fences &&
node.value &&
// If theres no info…
!node.lang &&
// And theres a non-whitespace character…
/[^ \r\n]/.test(node.value) &&
// And the value doesnt start or end in a blank…
!/^[\t ]*(?:[\r\n]|$)|(?:^|[\r\n])[\t ]*$/.test(node.value)
)
}

Some files were not shown because too many files have changed in this diff Show More