mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-21 09:16:30 -05:00
feat: import markdown-it-plugins from https://github.com/hedgedoc/markdown-it-plugins
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
1d90013344
commit
f5736dad0f
37 changed files with 2025 additions and 0 deletions
208
markdown-it-plugins/.editorconfig
Normal file
208
markdown-it-plugins/.editorconfig
Normal file
|
@ -0,0 +1,208 @@
|
|||
# SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
||||
#
|
||||
# SPDX-License-Identifier: CC0-1.0
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
tab_width = 4
|
||||
trim_trailing_whitespace = true
|
||||
ij_continuation_indent_size = 8
|
||||
ij_formatter_off_tag = @formatter:off
|
||||
ij_formatter_on_tag = @formatter:on
|
||||
ij_formatter_tags_enabled = false
|
||||
ij_smart_tabs = false
|
||||
ij_wrap_on_typing = false
|
||||
|
||||
[{*.ats,*.ts,*.tsx}]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
ij_continuation_indent_size = 2
|
||||
ij_typescript_align_imports = false
|
||||
ij_typescript_align_multiline_array_initializer_expression = false
|
||||
ij_typescript_align_multiline_binary_operation = false
|
||||
ij_typescript_align_multiline_chained_methods = false
|
||||
ij_typescript_align_multiline_extends_list = false
|
||||
ij_typescript_align_multiline_for = true
|
||||
ij_typescript_align_multiline_parameters = true
|
||||
ij_typescript_align_multiline_parameters_in_calls = false
|
||||
ij_typescript_align_multiline_ternary_operation = false
|
||||
ij_typescript_align_object_properties = 0
|
||||
ij_typescript_align_union_types = false
|
||||
ij_typescript_align_var_statements = 0
|
||||
ij_typescript_array_initializer_new_line_after_left_brace = false
|
||||
ij_typescript_array_initializer_right_brace_on_new_line = false
|
||||
ij_typescript_array_initializer_wrap = off
|
||||
ij_typescript_assignment_wrap = off
|
||||
ij_typescript_binary_operation_sign_on_next_line = false
|
||||
ij_typescript_binary_operation_wrap = off
|
||||
ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/**
|
||||
ij_typescript_blank_lines_after_imports = 1
|
||||
ij_typescript_blank_lines_around_class = 1
|
||||
ij_typescript_blank_lines_around_field = 0
|
||||
ij_typescript_blank_lines_around_field_in_interface = 0
|
||||
ij_typescript_blank_lines_around_function = 1
|
||||
ij_typescript_blank_lines_around_method = 1
|
||||
ij_typescript_blank_lines_around_method_in_interface = 1
|
||||
ij_typescript_block_brace_style = end_of_line
|
||||
ij_typescript_call_parameters_new_line_after_left_paren = false
|
||||
ij_typescript_call_parameters_right_paren_on_new_line = false
|
||||
ij_typescript_call_parameters_wrap = off
|
||||
ij_typescript_catch_on_new_line = false
|
||||
ij_typescript_chained_call_dot_on_new_line = true
|
||||
ij_typescript_class_brace_style = end_of_line
|
||||
ij_typescript_comma_on_new_line = false
|
||||
ij_typescript_do_while_brace_force = never
|
||||
ij_typescript_else_on_new_line = false
|
||||
ij_typescript_enforce_trailing_comma = keep
|
||||
ij_typescript_extends_keyword_wrap = off
|
||||
ij_typescript_extends_list_wrap = off
|
||||
ij_typescript_field_prefix = _
|
||||
ij_typescript_file_name_style = relaxed
|
||||
ij_typescript_finally_on_new_line = false
|
||||
ij_typescript_for_brace_force = never
|
||||
ij_typescript_for_statement_new_line_after_left_paren = false
|
||||
ij_typescript_for_statement_right_paren_on_new_line = false
|
||||
ij_typescript_for_statement_wrap = off
|
||||
ij_typescript_force_quote_style = false
|
||||
ij_typescript_force_semicolon_style = true
|
||||
ij_typescript_function_expression_brace_style = end_of_line
|
||||
ij_typescript_if_brace_force = never
|
||||
ij_typescript_import_merge_members = global
|
||||
ij_typescript_import_prefer_absolute_path = global
|
||||
ij_typescript_import_sort_members = true
|
||||
ij_typescript_import_sort_module_name = true
|
||||
ij_typescript_import_use_node_resolution = true
|
||||
ij_typescript_imports_wrap = on_every_item
|
||||
ij_typescript_indent_case_from_switch = true
|
||||
ij_typescript_indent_chained_calls = false
|
||||
ij_typescript_indent_package_children = 0
|
||||
ij_typescript_jsdoc_include_types = false
|
||||
ij_typescript_jsx_attribute_value = braces
|
||||
ij_typescript_keep_blank_lines_in_code = 1
|
||||
ij_typescript_keep_first_column_comment = true
|
||||
ij_typescript_keep_indents_on_empty_lines = false
|
||||
ij_typescript_keep_line_breaks = true
|
||||
ij_typescript_keep_simple_blocks_in_one_line = false
|
||||
ij_typescript_keep_simple_methods_in_one_line = false
|
||||
ij_typescript_line_comment_add_space = true
|
||||
ij_typescript_line_comment_at_first_column = false
|
||||
ij_typescript_method_brace_style = end_of_line
|
||||
ij_typescript_method_call_chain_wrap = off
|
||||
ij_typescript_method_parameters_new_line_after_left_paren = false
|
||||
ij_typescript_method_parameters_right_paren_on_new_line = false
|
||||
ij_typescript_method_parameters_wrap = off
|
||||
ij_typescript_object_literal_wrap = on_every_item
|
||||
ij_typescript_parentheses_expression_new_line_after_left_paren = false
|
||||
ij_typescript_parentheses_expression_right_paren_on_new_line = false
|
||||
ij_typescript_place_assignment_sign_on_next_line = false
|
||||
ij_typescript_prefer_as_type_cast = false
|
||||
ij_typescript_prefer_explicit_types_function_expression_returns = false
|
||||
ij_typescript_prefer_explicit_types_function_returns = false
|
||||
ij_typescript_prefer_explicit_types_vars_fields = false
|
||||
ij_typescript_prefer_parameters_wrap = false
|
||||
ij_typescript_reformat_c_style_comments = false
|
||||
ij_typescript_space_after_colon = true
|
||||
ij_typescript_space_after_comma = true
|
||||
ij_typescript_space_after_dots_in_rest_parameter = false
|
||||
ij_typescript_space_after_generator_mult = true
|
||||
ij_typescript_space_after_property_colon = true
|
||||
ij_typescript_space_after_quest = true
|
||||
ij_typescript_space_after_type_colon = true
|
||||
ij_typescript_space_after_unary_not = false
|
||||
ij_typescript_space_before_async_arrow_lparen = true
|
||||
ij_typescript_space_before_catch_keyword = true
|
||||
ij_typescript_space_before_catch_left_brace = true
|
||||
ij_typescript_space_before_catch_parentheses = true
|
||||
ij_typescript_space_before_class_lbrace = true
|
||||
ij_typescript_space_before_class_left_brace = true
|
||||
ij_typescript_space_before_colon = true
|
||||
ij_typescript_space_before_comma = false
|
||||
ij_typescript_space_before_do_left_brace = true
|
||||
ij_typescript_space_before_else_keyword = true
|
||||
ij_typescript_space_before_else_left_brace = true
|
||||
ij_typescript_space_before_finally_keyword = true
|
||||
ij_typescript_space_before_finally_left_brace = true
|
||||
ij_typescript_space_before_for_left_brace = true
|
||||
ij_typescript_space_before_for_parentheses = true
|
||||
ij_typescript_space_before_for_semicolon = false
|
||||
ij_typescript_space_before_function_left_parenth = true
|
||||
ij_typescript_space_before_generator_mult = false
|
||||
ij_typescript_space_before_if_left_brace = true
|
||||
ij_typescript_space_before_if_parentheses = true
|
||||
ij_typescript_space_before_method_call_parentheses = false
|
||||
ij_typescript_space_before_method_left_brace = true
|
||||
ij_typescript_space_before_method_parentheses = true
|
||||
ij_typescript_space_before_property_colon = false
|
||||
ij_typescript_space_before_quest = true
|
||||
ij_typescript_space_before_switch_left_brace = true
|
||||
ij_typescript_space_before_switch_parentheses = true
|
||||
ij_typescript_space_before_try_left_brace = true
|
||||
ij_typescript_space_before_type_colon = false
|
||||
ij_typescript_space_before_unary_not = false
|
||||
ij_typescript_space_before_while_keyword = true
|
||||
ij_typescript_space_before_while_left_brace = true
|
||||
ij_typescript_space_before_while_parentheses = true
|
||||
ij_typescript_spaces_around_additive_operators = true
|
||||
ij_typescript_spaces_around_arrow_function_operator = true
|
||||
ij_typescript_spaces_around_assignment_operators = true
|
||||
ij_typescript_spaces_around_bitwise_operators = true
|
||||
ij_typescript_spaces_around_equality_operators = true
|
||||
ij_typescript_spaces_around_logical_operators = true
|
||||
ij_typescript_spaces_around_multiplicative_operators = true
|
||||
ij_typescript_spaces_around_relational_operators = true
|
||||
ij_typescript_spaces_around_shift_operators = true
|
||||
ij_typescript_spaces_around_unary_operator = false
|
||||
ij_typescript_spaces_within_array_initializer_brackets = false
|
||||
ij_typescript_spaces_within_brackets = false
|
||||
ij_typescript_spaces_within_catch_parentheses = false
|
||||
ij_typescript_spaces_within_for_parentheses = false
|
||||
ij_typescript_spaces_within_if_parentheses = false
|
||||
ij_typescript_spaces_within_imports = true
|
||||
ij_typescript_spaces_within_interpolation_expressions = false
|
||||
ij_typescript_spaces_within_method_call_parentheses = false
|
||||
ij_typescript_spaces_within_method_parentheses = false
|
||||
ij_typescript_spaces_within_object_literal_braces = true
|
||||
ij_typescript_spaces_within_object_type_braces = true
|
||||
ij_typescript_spaces_within_parentheses = false
|
||||
ij_typescript_spaces_within_switch_parentheses = false
|
||||
ij_typescript_spaces_within_type_assertion = false
|
||||
ij_typescript_spaces_within_union_types = true
|
||||
ij_typescript_spaces_within_while_parentheses = false
|
||||
ij_typescript_special_else_if_treatment = true
|
||||
ij_typescript_ternary_operation_signs_on_next_line = false
|
||||
ij_typescript_ternary_operation_wrap = off
|
||||
ij_typescript_union_types_wrap = on_every_item
|
||||
ij_typescript_use_chained_calls_group_indents = false
|
||||
ij_typescript_use_double_quotes = false
|
||||
ij_typescript_use_explicit_js_extension = global
|
||||
ij_typescript_use_path_mapping = always
|
||||
ij_typescript_use_public_modifier = false
|
||||
ij_typescript_use_semicolon_after_statement = false
|
||||
ij_typescript_var_declaration_wrap = normal
|
||||
ij_typescript_while_brace_force = never
|
||||
ij_typescript_while_on_new_line = false
|
||||
ij_typescript_wrap_comments = false
|
||||
|
||||
[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}]
|
||||
indent_size = 2
|
||||
ij_json_keep_blank_lines_in_code = 0
|
||||
ij_json_keep_indents_on_empty_lines = false
|
||||
ij_json_keep_line_breaks = true
|
||||
ij_json_space_after_colon = true
|
||||
ij_json_space_after_comma = true
|
||||
ij_json_space_before_colon = true
|
||||
ij_json_space_before_comma = false
|
||||
ij_json_spaces_within_braces = false
|
||||
ij_json_spaces_within_brackets = false
|
||||
ij_json_wrap_long_lines = false
|
||||
|
||||
[{*.yaml,*.yml}]
|
||||
indent_size = 2
|
||||
ij_yaml_keep_indents_on_empty_lines = false
|
||||
ij_yaml_keep_line_breaks = true
|
||||
|
36
markdown-it-plugins/.eslintrc.json
Normal file
36
markdown-it-plugins/.eslintrc.json
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: CC0-1.0
|
||||
*/
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"./tsconfig.test.json"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"jest",
|
||||
"prettier"
|
||||
],
|
||||
"env": {
|
||||
"jest": true,
|
||||
"jest/globals": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"jest/no-disabled-tests": "warn",
|
||||
"jest/no-focused-tests": "error",
|
||||
"jest/no-identical-title": "error",
|
||||
"jest/prefer-to-have-length": "warn",
|
||||
"jest/valid-expect": "error"
|
||||
}
|
||||
}
|
1
markdown-it-plugins/.gitignore
vendored
Normal file
1
markdown-it-plugins/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
dist/
|
11
markdown-it-plugins/.prettierrc.json
Normal file
11
markdown-it-plugins/.prettierrc.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"parser": "typescript",
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": true,
|
||||
"arrowParens": "always"
|
||||
}
|
3
markdown-it-plugins/.prettierrc.json.license
Normal file
3
markdown-it-plugins/.prettierrc.json.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
49
markdown-it-plugins/.renovaterc.json
Normal file
49
markdown-it-plugins/.renovaterc.json
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"extends": [
|
||||
"config:base",
|
||||
":gitSignOff",
|
||||
":preserveSemverRanges",
|
||||
":prHourlyLimitNone",
|
||||
":dependencyDashboard",
|
||||
":maintainLockFilesWeekly"
|
||||
],
|
||||
"prHourlyLimit": 0,
|
||||
"schedule": [
|
||||
"on Saturday"
|
||||
],
|
||||
"labels": [
|
||||
"dependencies"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"extends": "packages:linters",
|
||||
"groupName": "linters"
|
||||
},
|
||||
{
|
||||
"extends": "monorepo:typescript-eslint",
|
||||
"groupName": "typescript-eslint monorepo"
|
||||
},
|
||||
{
|
||||
"extends": "monorepo:react",
|
||||
"groupName": "react monorepo"
|
||||
},
|
||||
{
|
||||
"extends": "monorepo:reactrouter",
|
||||
"groupName": "reactrouter monorepo"
|
||||
},
|
||||
{
|
||||
"groupName": "definitelyTyped",
|
||||
"matchPackagePatterns": [
|
||||
"^@types/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupName": "Jest",
|
||||
"matchPackageNames": [
|
||||
"jest",
|
||||
"ts-jest",
|
||||
"@types/jest"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
3
markdown-it-plugins/.renovaterc.json.license
Normal file
3
markdown-it-plugins/.renovaterc.json.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
21
markdown-it-plugins/LICENSE
Normal file
21
markdown-it-plugins/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
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.
|
143
markdown-it-plugins/README.md
Normal file
143
markdown-it-plugins/README.md
Normal file
|
@ -0,0 +1,143 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
-->
|
||||
|
||||
# Markdown-It plugins
|
||||
|
||||
This repository contains plugins for [Markdown-It](https://github.com/markdown-it/markdown-it) that are used by HedgeDoc.
|
||||
All these plugins are (re)created in typescript.
|
||||
|
||||
## License
|
||||
Everything is licensed under MIT
|
||||
|
||||
## Usage
|
||||
Install the lib using `yarn install @hedgedoc/markdown-it-plugins` or `npm i @hedgedoc/markdown-it-plugins`
|
||||
|
||||
## Development
|
||||
If you want to contribute to this lib then:
|
||||
- Clone this repository
|
||||
- Install the dependencies using `yarn install`. Don't use `npm`!
|
||||
- Make your changes
|
||||
- Make sure that your changes are covered by tests. Use `yarn test` to run all tests
|
||||
- Make sure that your code follows the code style. Use `yarn lint` to check the style
|
||||
- Commit your changes (please use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)) and create a pull request
|
||||
|
||||
## markdown-it-image-size
|
||||
A markdown-it plugin for size-specified image markups. This plugin overloads the original image renderer of markdown-it.
|
||||
> This is a typescript port of https://github.com/tatsy/markdown-it-imsize without the local file system support.
|
||||
|
||||
### Usage
|
||||
|
||||
#### Enable plugin
|
||||
|
||||
```ts
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { imageSize } from '@hedgedoc/markdown-it-plugins'
|
||||
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typography: true
|
||||
}).use(imageSize);
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
```md
|
||||
![test](image.png =100x200)
|
||||
```
|
||||
|
||||
is interpreted as
|
||||
|
||||
```html
|
||||
<p><img src="image.png" alt="test" width="100" height="200"></p>
|
||||
```
|
||||
|
||||
## markdown-it-better-task-lists
|
||||
|
||||
A markdown-it plugin that renders GitHub-style task-lists. It builds [task/todo lists](https://github.com/blog/1825-task-lists-in-all-markdown-documents) out of Markdown lists with items starting with `[ ]` or `[x]`.
|
||||
|
||||
This is a typescript port of [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists).
|
||||
|
||||
### Why is this useful?
|
||||
|
||||
When you have markdown documentation with checklists, rendering HTML checkboxes
|
||||
out of the list items looks nicer than the raw square brackets.
|
||||
|
||||
### Usage
|
||||
|
||||
Use it the same as a normal markdown-it plugin:
|
||||
|
||||
```ts
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { taskLists } from '@hedgedoc/markdown-it-plugins'
|
||||
|
||||
const parser = new MarkdownIt().use(taskLists)
|
||||
|
||||
const result = parser.render(`
|
||||
- [ ] Open task
|
||||
- [x] Done task
|
||||
- Not a task
|
||||
`) // markdown string containing task list items
|
||||
```
|
||||
|
||||
The rendered checkboxes are disabled; to change this, set `enabled` property of the
|
||||
plugin options to `true`:
|
||||
|
||||
```ts
|
||||
const parser = new MarkdownIt().use(taskLists, { enabled: true })
|
||||
```
|
||||
|
||||
If you need to know which line in the markdown document the generated checkbox comes
|
||||
set the `lineNumber` property of the plugin options to `true` for the
|
||||
`<input>` tag to be created with a data-line attribute containing the line number:
|
||||
|
||||
```ts
|
||||
const parser = new MarkdownIt().use(taskLists, { lineNumber: true })
|
||||
```
|
||||
|
||||
If you'd like to wrap the rendered list items in a `<label>` element for UX
|
||||
purposes, set the `label` property of the plugin options to `true`:
|
||||
|
||||
```ts
|
||||
const parser = new MarkdownIt().use(taskLists, { label: true })
|
||||
```
|
||||
|
||||
To add the label after the checkbox set the `labelAfter` property of the plugin
|
||||
options to `true`:
|
||||
|
||||
```ts
|
||||
const parser = new MarkdownIt().use(taskLists, { label: true, labelAfter: true })
|
||||
```
|
||||
|
||||
**Note:** This option does require the `label` option to be truthy.
|
||||
|
||||
The options can be combined, of course.
|
||||
|
||||
## markdown-it-toc
|
||||
|
||||
A markdown-it plugin that renders a table of contents.
|
||||
It uses the found headlines as content.
|
||||
|
||||
This is a typescript port of [markdown-it-toc-done-right](https://github.com/nagaozen/markdown-it-toc-done-right).
|
||||
|
||||
### Usage
|
||||
|
||||
Use it the same as a normal markdown-it plugin:
|
||||
|
||||
```ts
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import { toc } from '@hedgedoc/markdown-it-plugins'
|
||||
|
||||
const parser = new MarkdownIt().use(toc)
|
||||
|
||||
const result = parser.render(`
|
||||
[toc]
|
||||
|
||||
# head 1
|
||||
|
||||
# head 2
|
||||
`) // markdown string containing task list items
|
||||
```
|
34
markdown-it-plugins/build.sh
Executable file
34
markdown-it-plugins/build.sh
Executable file
|
@ -0,0 +1,34 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
#
|
||||
|
||||
|
||||
set -e
|
||||
|
||||
echo "Clear dist directory.."
|
||||
rm -rf dist
|
||||
|
||||
echo "Compile to CJS.."
|
||||
tsc --project tsconfig.cjs.json
|
||||
|
||||
echo "Compile to ESM.."
|
||||
tsc --project tsconfig.esm.json
|
||||
|
||||
echo "Fix CJS package.json.."
|
||||
cat > dist/cjs/package.json <<!EOF
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
!EOF
|
||||
|
||||
echo "Fix ESM package.json.."
|
||||
cat > dist/esm/package.json <<!EOF
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
!EOF
|
||||
|
||||
echo "Done!"
|
26
markdown-it-plugins/jest.config.json
Normal file
26
markdown-it-plugins/jest.config.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"testRegex" : "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
|
||||
"testPathIgnorePatterns" : [
|
||||
"/dist/"
|
||||
],
|
||||
"moduleFileExtensions" : [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js"
|
||||
],
|
||||
"extensionsToTreatAsEsm" : [
|
||||
".ts"
|
||||
],
|
||||
"moduleNameMapper" : {
|
||||
"^(\\.{1,2}/.*)\\.js$" : "$1"
|
||||
},
|
||||
"transform" : {
|
||||
"^.+\\.tsx?$" : [
|
||||
"ts-jest",
|
||||
{
|
||||
"tsconfig" : "tsconfig.test.json",
|
||||
"useESM" : true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
3
markdown-it-plugins/jest.config.json.license
Normal file
3
markdown-it-plugins/jest.config.json.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
133
markdown-it-plugins/package.json
Normal file
133
markdown-it-plugins/package.json
Normal file
|
@ -0,0 +1,133 @@
|
|||
{
|
||||
"name": "@hedgedoc/markdown-it-plugins",
|
||||
"type": "module",
|
||||
"version": "2.1.3",
|
||||
"description": "A collection of used and modified markdown-it plugins.",
|
||||
"source": "src/index.ts",
|
||||
"main": "../dist/cjs/index.js",
|
||||
"types": "dist/cjs/index.d.ts",
|
||||
"module": "../dist/esm/index.js",
|
||||
"exports": {
|
||||
"import": {
|
||||
"types": "./dist/esm/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/index.d.ts",
|
||||
"default": "./dist/cjs/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"LICENSES/*",
|
||||
"package.json",
|
||||
"README.md",
|
||||
"dist/cjs/image-size/index.d.ts",
|
||||
"dist/cjs/image-size/index.js",
|
||||
"dist/cjs/image-size/index.js.map",
|
||||
"dist/cjs/image-size/parse-image-size.d.ts",
|
||||
"dist/cjs/image-size/parse-image-size.js",
|
||||
"dist/cjs/image-size/parse-image-size.js.map",
|
||||
"dist/cjs/image-size/specialCharacters.d.ts",
|
||||
"dist/cjs/image-size/specialCharacters.js",
|
||||
"dist/cjs/image-size/specialCharacters.js.map",
|
||||
"dist/cjs/index.d.ts",
|
||||
"dist/cjs/index.js",
|
||||
"dist/cjs/index.js.map",
|
||||
"dist/cjs/package.json",
|
||||
"dist/cjs/task-lists/index.d.ts",
|
||||
"dist/cjs/task-lists/index.js",
|
||||
"dist/cjs/task-lists/index.js.map",
|
||||
"dist/cjs/toc/index.d.ts",
|
||||
"dist/cjs/toc/index.js",
|
||||
"dist/cjs/toc/index.js.map",
|
||||
"dist/cjs/toc/plugin.d.ts",
|
||||
"dist/cjs/toc/plugin.js",
|
||||
"dist/cjs/toc/plugin.js.map",
|
||||
"dist/cjs/toc/toc-ast.d.ts",
|
||||
"dist/cjs/toc/toc-ast.js",
|
||||
"dist/cjs/toc/toc-ast.js.map",
|
||||
"dist/cjs/toc/toc-body-renderer.d.ts",
|
||||
"dist/cjs/toc/toc-body-renderer.js",
|
||||
"dist/cjs/toc/toc-body-renderer.js.map",
|
||||
"dist/cjs/toc/toc-options.d.ts",
|
||||
"dist/cjs/toc/toc-options.js",
|
||||
"dist/cjs/toc/toc-options.js.map",
|
||||
"dist/esm/image-size/index.d.ts",
|
||||
"dist/esm/image-size/index.js",
|
||||
"dist/esm/image-size/index.js.map",
|
||||
"dist/esm/image-size/parse-image-size.d.ts",
|
||||
"dist/esm/image-size/parse-image-size.js",
|
||||
"dist/esm/image-size/parse-image-size.js.map",
|
||||
"dist/esm/image-size/specialCharacters.d.ts",
|
||||
"dist/esm/image-size/specialCharacters.js",
|
||||
"dist/esm/image-size/specialCharacters.js.map",
|
||||
"dist/esm/index.d.ts",
|
||||
"dist/esm/index.js",
|
||||
"dist/esm/index.js.map",
|
||||
"dist/esm/package.json",
|
||||
"dist/esm/task-lists/index.d.ts",
|
||||
"dist/esm/task-lists/index.js",
|
||||
"dist/esm/task-lists/index.js.map",
|
||||
"dist/esm/toc/index.d.ts",
|
||||
"dist/esm/toc/index.js",
|
||||
"dist/esm/toc/index.js.map",
|
||||
"dist/esm/toc/plugin.d.ts",
|
||||
"dist/esm/toc/plugin.js",
|
||||
"dist/esm/toc/plugin.js.map",
|
||||
"dist/esm/toc/toc-ast.d.ts",
|
||||
"dist/esm/toc/toc-ast.js",
|
||||
"dist/esm/toc/toc-ast.js.map",
|
||||
"dist/esm/toc/toc-body-renderer.d.ts",
|
||||
"dist/esm/toc/toc-body-renderer.js",
|
||||
"dist/esm/toc/toc-body-renderer.js.map",
|
||||
"dist/esm/toc/toc-options.d.ts",
|
||||
"dist/esm/toc/toc-options.js",
|
||||
"dist/esm/toc/toc-options.js.map"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "../node_modules/.bin/jest",
|
||||
"build": "build.sh",
|
||||
"prepublish": "rm -rf ../dist && yarn lint && yarn build && yarn test",
|
||||
"lint": "../node_modules/.bin/eslint --ext .ts markdown-it-plugins/src",
|
||||
"lint:fix": "../node_modules/.bin/eslint --fix --ext .ts markdown-it-plugins/src"
|
||||
},
|
||||
"keywords": [
|
||||
"markdown",
|
||||
"markdown-it",
|
||||
"image size",
|
||||
"task lists"
|
||||
],
|
||||
"author": "The HedgeDoc developers",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"markdown-it": ">=12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mrdrogdrog/optional": "^1.2.1",
|
||||
"html-entities": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/types": "29.6.3",
|
||||
"@types/markdown-it": "13.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.4.1",
|
||||
"@typescript-eslint/parser": "6.4.1",
|
||||
"eslint": "8.48.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
"eslint-plugin-jest": "27.2.3",
|
||||
"eslint-plugin-prettier": "5.0.0",
|
||||
"jest": "29.6.4",
|
||||
"markdown-it": "13.0.1",
|
||||
"prettier": "3.0.2",
|
||||
"ts-jest": "29.1.1",
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/hedgedoc/markdown-it-plugins.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/hedgedoc/markdown-it-plugins/issues"
|
||||
},
|
||||
"homepage": "https://github.com/hedgedoc/markdown-it-plugins#readme",
|
||||
"packageManager": "yarn@3.6.3"
|
||||
}
|
3
markdown-it-plugins/package.json.license
Normal file
3
markdown-it-plugins/package.json.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2020 The HedgeDoc developers (see AUTHORS file)
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
56
markdown-it-plugins/src/image-size/index.test.ts
Normal file
56
markdown-it-plugins/src/image-size/index.test.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it/lib'
|
||||
|
||||
import { imageSize } from './index.js'
|
||||
import { describe, expect, it } from '@jest/globals'
|
||||
|
||||
describe('markdown-it-imsize', function () {
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true
|
||||
}).use(imageSize)
|
||||
|
||||
it('renders a image without size or title', () => {
|
||||
expect(md.renderInline('![test](x)')).toBe('<img src="x" alt="test">')
|
||||
})
|
||||
|
||||
it('renders a image with title', () => {
|
||||
expect(md.renderInline('![test](x "thisisthetitle")')).toBe('<img src="x" alt="test" title="thisisthetitle">')
|
||||
})
|
||||
|
||||
it('renders an image with absolute width and height', () => {
|
||||
expect(md.renderInline('![test](x =100x200)')).toBe('<img src="x" alt="test" width="100" height="200">')
|
||||
})
|
||||
|
||||
it('renders an image with relative width and height', () => {
|
||||
expect(md.renderInline('![test](x =100%x200%)')).toBe('<img src="x" alt="test" width="100%" height="200%">')
|
||||
})
|
||||
|
||||
it('renders an image with title and size', () => {
|
||||
expect(md.renderInline('![test](x "thisisthetitle" =100x200)')).toBe(
|
||||
'<img src="x" alt="test" title="thisisthetitle" width="100" height="200">'
|
||||
)
|
||||
})
|
||||
|
||||
it('renders an image with no size but x', () => {
|
||||
expect(md.renderInline('![test](x "thisisthetitle" =x)')).toBe('<img src="x" alt="test" title="thisisthetitle">')
|
||||
})
|
||||
|
||||
it("doesn't render an image with invalid size syntax", () => {
|
||||
expect(md.renderInline('![test](x "thisisthetitle" =xx)')).toBe('![test](x “thisisthetitle” =xx)')
|
||||
})
|
||||
|
||||
it('renders an image with only width', () => {
|
||||
expect(md.renderInline('![test](x =100x)')).toBe('<img src="x" alt="test" width="100">')
|
||||
})
|
||||
|
||||
it('renders an image with only height', () => {
|
||||
expect(md.renderInline('![test](x =x200)')).toBe('<img src="x" alt="test" height="200">')
|
||||
})
|
||||
})
|
245
markdown-it-plugins/src/image-size/index.ts
Normal file
245
markdown-it-plugins/src/image-size/index.ts
Normal file
|
@ -0,0 +1,245 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import ParserInline from 'markdown-it/lib/parser_inline.js'
|
||||
import StateInline from 'markdown-it/lib/rules_inline/state_inline.js'
|
||||
import { ParseImageSize, parseImageSize } from './parse-image-size.js'
|
||||
import { SpecialCharacters } from './specialCharacters.js'
|
||||
|
||||
const checkForImageTagStart = (state: StateInline): boolean => {
|
||||
return (
|
||||
state.src.charCodeAt(state.pos) === SpecialCharacters.EXCLAMATION_MARK &&
|
||||
state.src.charCodeAt(state.pos + 1) === SpecialCharacters.OPENING_BRACKET
|
||||
)
|
||||
}
|
||||
|
||||
const skipWhiteSpaces = (startPosition: number, state: StateInline): number => {
|
||||
let position = startPosition
|
||||
while (position < state.posMax) {
|
||||
const code = state.src.charCodeAt(position)
|
||||
if (code !== SpecialCharacters.WHITESPACE && code !== SpecialCharacters.NEW_LINE) {
|
||||
break
|
||||
}
|
||||
position += 1
|
||||
}
|
||||
return position
|
||||
}
|
||||
|
||||
function createImageToken(
|
||||
state: StateInline,
|
||||
labelStartIndex: number,
|
||||
labelEndIndex: number,
|
||||
href: string,
|
||||
title: string,
|
||||
width: string,
|
||||
height: string
|
||||
) {
|
||||
state.pos = labelStartIndex
|
||||
state.posMax = labelEndIndex
|
||||
|
||||
const token = state.push('image', 'img', 0)
|
||||
token.children = []
|
||||
|
||||
const newState = new state.md.inline.State(
|
||||
state.src.slice(labelStartIndex, labelEndIndex),
|
||||
state.md,
|
||||
state.env,
|
||||
token.children
|
||||
)
|
||||
newState.md.inline.tokenize(newState)
|
||||
|
||||
token.attrSet('src', href)
|
||||
token.attrSet('alt', '')
|
||||
|
||||
if (title) {
|
||||
token.attrSet('title', title)
|
||||
}
|
||||
|
||||
if (width !== '') {
|
||||
token.attrSet('width', width)
|
||||
}
|
||||
|
||||
if (height !== '') {
|
||||
token.attrSet('height', height)
|
||||
}
|
||||
}
|
||||
|
||||
function parseSizeParameters(startPosition: number, state: StateInline): ParseImageSize | undefined {
|
||||
// [link]( <href> "title" =WxH )
|
||||
// ^^^^ parsing image size
|
||||
if (startPosition - 1 < 0) {
|
||||
return
|
||||
}
|
||||
const code = state.src.charCodeAt(startPosition - 1)
|
||||
if (code !== SpecialCharacters.WHITESPACE) {
|
||||
return
|
||||
}
|
||||
const res = parseImageSize(state.src, startPosition, state.posMax)
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
// [link]( <href> "title" =WxH )
|
||||
// ^^ skipping these spaces
|
||||
return {
|
||||
position: skipWhiteSpaces(res.position, state),
|
||||
width: res.width,
|
||||
height: res.height
|
||||
}
|
||||
}
|
||||
|
||||
export interface ParseLinkResult {
|
||||
position: number
|
||||
href: string
|
||||
}
|
||||
|
||||
// [link]( <href> "title" )
|
||||
// ^^^^^^ parsing link destination
|
||||
function parseLink(state: StateInline, startPosition: number): ParseLinkResult | undefined {
|
||||
const linkParseResult = state.md.helpers.parseLinkDestination(state.src, startPosition, state.posMax)
|
||||
if (!linkParseResult.ok) {
|
||||
return
|
||||
}
|
||||
const href = state.md.normalizeLink(linkParseResult.str)
|
||||
if (state.md.validateLink(href)) {
|
||||
return { position: linkParseResult.pos, href }
|
||||
} else {
|
||||
return { position: startPosition, href: '' }
|
||||
}
|
||||
}
|
||||
|
||||
const imageWithSize: ParserInline.RuleInline = (state, silent) => {
|
||||
let position,
|
||||
title,
|
||||
start,
|
||||
href = '',
|
||||
width = '',
|
||||
height = ''
|
||||
const oldPos = state.pos,
|
||||
max = state.posMax
|
||||
|
||||
if (!checkForImageTagStart(state)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const labelStartIndex = state.pos + 2
|
||||
const labelEndIndex = state.md.helpers.parseLinkLabel(state, state.pos + 1, false)
|
||||
|
||||
// parser failed to find ']', so it's not a valid link
|
||||
if (labelEndIndex < 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
position = labelEndIndex + 1
|
||||
if (position < max && state.src.charCodeAt(position) === SpecialCharacters.OPENING_PARENTHESIS) {
|
||||
//
|
||||
// Inline link
|
||||
//
|
||||
|
||||
// [link]( <href> "title" )
|
||||
// ^^ skipping these spaces
|
||||
position += 1
|
||||
position = skipWhiteSpaces(position, state)
|
||||
if (position >= max) {
|
||||
return false
|
||||
}
|
||||
|
||||
const parseLinkResult = parseLink(state, position)
|
||||
if (!parseLinkResult) {
|
||||
return false
|
||||
}
|
||||
position = parseLinkResult.position
|
||||
href = parseLinkResult.href
|
||||
|
||||
// [link]( <href> "title" )
|
||||
// ^^ skipping these spaces
|
||||
start = position
|
||||
position = skipWhiteSpaces(position, state)
|
||||
|
||||
// [link]( <href> "title" )
|
||||
// ^^^^^^^ parsing link title
|
||||
const parseLinkTitleResult = state.md.helpers.parseLinkTitle(state.src, position, state.posMax)
|
||||
if (position < max && start !== position && parseLinkTitleResult.ok) {
|
||||
title = parseLinkTitleResult.str
|
||||
position = parseLinkTitleResult.pos
|
||||
|
||||
// [link]( <href> "title" )
|
||||
// ^^ skipping these spaces
|
||||
position = skipWhiteSpaces(position, state)
|
||||
} else {
|
||||
title = ''
|
||||
}
|
||||
|
||||
const parseSizeParametersResult = parseSizeParameters(position, state)
|
||||
if (parseSizeParametersResult) {
|
||||
position = parseSizeParametersResult.position
|
||||
width = parseSizeParametersResult.width
|
||||
height = parseSizeParametersResult.height
|
||||
}
|
||||
|
||||
if (position >= max || state.src.charCodeAt(position) !== SpecialCharacters.CLOSING_PARENTHESIS) {
|
||||
state.pos = oldPos
|
||||
return false
|
||||
}
|
||||
position += 1
|
||||
} else {
|
||||
//
|
||||
// Link reference
|
||||
//
|
||||
if (typeof state.env.references === 'undefined') {
|
||||
return false
|
||||
}
|
||||
|
||||
// [foo] [bar]
|
||||
// ^^ optional whitespace (can include newlines)
|
||||
position = skipWhiteSpaces(position, state)
|
||||
|
||||
let label
|
||||
|
||||
if (position < max && state.src.charCodeAt(position) === SpecialCharacters.OPENING_BRACKET) {
|
||||
start = position + 1
|
||||
position = state.md.helpers.parseLinkLabel(state, position)
|
||||
if (position >= 0) {
|
||||
label = state.src.slice(start, (position += 1))
|
||||
} else {
|
||||
position = labelEndIndex + 1
|
||||
}
|
||||
} else {
|
||||
position = labelEndIndex + 1
|
||||
}
|
||||
|
||||
// covers label === '' and label === undefined
|
||||
// (collapsed reference link and shortcut reference link respectively)
|
||||
if (!label) {
|
||||
label = state.src.slice(labelStartIndex, labelEndIndex)
|
||||
}
|
||||
|
||||
const ref = state.env.references[state.md.utils.normalizeReference(label)]
|
||||
if (!ref) {
|
||||
state.pos = oldPos
|
||||
return false
|
||||
}
|
||||
href = ref.href
|
||||
title = ref.title
|
||||
}
|
||||
|
||||
//
|
||||
// We found the end of the link, and know for a fact it's a valid link;
|
||||
// so all that's left to do is to call tokenizer.
|
||||
//
|
||||
if (!silent) {
|
||||
createImageToken(state, labelStartIndex, labelEndIndex, href, title, width, height)
|
||||
}
|
||||
|
||||
state.pos = position
|
||||
state.posMax = max
|
||||
return true
|
||||
}
|
||||
|
||||
export const imageSize: MarkdownIt.PluginSimple = (md: MarkdownIt) => {
|
||||
md.inline.ruler.before('emphasis', 'image', imageWithSize)
|
||||
}
|
98
markdown-it-plugins/src/image-size/parse-image-size.ts
Normal file
98
markdown-it-plugins/src/image-size/parse-image-size.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { SpecialCharacters } from './specialCharacters.js'
|
||||
|
||||
export interface ParseImageSize {
|
||||
position: number
|
||||
width: string
|
||||
height: string
|
||||
}
|
||||
|
||||
export interface ParseNextNumber {
|
||||
position: number
|
||||
value: string
|
||||
}
|
||||
|
||||
function isCharacterADigit(code: number) {
|
||||
return code >= SpecialCharacters.NUMBER_ZERO && code <= SpecialCharacters.NUMBER_NINE
|
||||
}
|
||||
|
||||
function findNextNotNumberCharacter(startPosition: number, maximalPosition: number, content: string): number {
|
||||
for (let position = startPosition; position < maximalPosition; position += 1) {
|
||||
const code = content.charCodeAt(position)
|
||||
if (!isCharacterADigit(code) && code !== SpecialCharacters.PERCENTAGE) {
|
||||
return position
|
||||
}
|
||||
}
|
||||
|
||||
return maximalPosition
|
||||
}
|
||||
|
||||
function parseNextNumber(content: string, startPosition: number, maximalPosition: number): ParseNextNumber {
|
||||
const endCharacterIndex = findNextNotNumberCharacter(startPosition, maximalPosition, content)
|
||||
|
||||
return {
|
||||
position: endCharacterIndex,
|
||||
value: content.slice(startPosition, endCharacterIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
size must follow = without any white spaces as follows
|
||||
(1) =300x200
|
||||
(2) =300x
|
||||
(3) =x200
|
||||
*/
|
||||
const checkImageSizeStart = (code: number): boolean => {
|
||||
return (
|
||||
code === SpecialCharacters.LOWER_CASE_X ||
|
||||
(code >= SpecialCharacters.NUMBER_ZERO && code <= SpecialCharacters.NUMBER_NINE)
|
||||
)
|
||||
}
|
||||
|
||||
export function parseImageSize(
|
||||
imageSize: string,
|
||||
startCharacterPosition: number,
|
||||
maximalCharacterPosition: number
|
||||
): ParseImageSize | undefined {
|
||||
if (startCharacterPosition >= maximalCharacterPosition) {
|
||||
return
|
||||
}
|
||||
|
||||
let currentCharacterPosition = startCharacterPosition
|
||||
|
||||
if (imageSize.charCodeAt(currentCharacterPosition) !== SpecialCharacters.EQUALS /* = */) {
|
||||
return
|
||||
}
|
||||
|
||||
currentCharacterPosition += 1
|
||||
|
||||
if (!checkImageSizeStart(imageSize.charCodeAt(currentCharacterPosition))) {
|
||||
return
|
||||
}
|
||||
|
||||
// parse width
|
||||
const width = parseNextNumber(imageSize, currentCharacterPosition, maximalCharacterPosition)
|
||||
currentCharacterPosition = width.position
|
||||
|
||||
// next charactor must be 'x'
|
||||
const code = imageSize.charCodeAt(currentCharacterPosition)
|
||||
if (code !== SpecialCharacters.LOWER_CASE_X /* x */) {
|
||||
return
|
||||
}
|
||||
currentCharacterPosition += 1
|
||||
|
||||
// parse height
|
||||
const height = parseNextNumber(imageSize, currentCharacterPosition, maximalCharacterPosition)
|
||||
currentCharacterPosition = height.position
|
||||
|
||||
return {
|
||||
width: width.value,
|
||||
height: height.value,
|
||||
position: currentCharacterPosition
|
||||
}
|
||||
}
|
19
markdown-it-plugins/src/image-size/specialCharacters.ts
Normal file
19
markdown-it-plugins/src/image-size/specialCharacters.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export enum SpecialCharacters {
|
||||
EXCLAMATION_MARK = 0x21,
|
||||
OPENING_BRACKET = 0x5b,
|
||||
OPENING_PARENTHESIS = 0x28,
|
||||
WHITESPACE = 0x20,
|
||||
NEW_LINE = 0x0a,
|
||||
EQUALS = 0x3d,
|
||||
LOWER_CASE_X = 0x78,
|
||||
NUMBER_ZERO = 0x30,
|
||||
NUMBER_NINE = 0x39,
|
||||
PERCENTAGE = 0x25,
|
||||
CLOSING_PARENTHESIS = 0x29
|
||||
}
|
9
markdown-it-plugins/src/index.ts
Normal file
9
markdown-it-plugins/src/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export { imageSize } from './image-size'
|
||||
export { taskLists } from './task-lists'
|
||||
export * from './toc'
|
|
@ -0,0 +1,67 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
exports[`markdown-it-task-lists renders bullet correctly 1`] = `
|
||||
"<ul class="contains-task-list">
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-1" />unchecked item 1</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-2" />unchecked item 2</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-3" />unchecked item 3</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" checked="" disabled="" id="task-item-4" />checked item 4</li>
|
||||
</ul>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`markdown-it-task-lists renders dirty correctly 1`] = `
|
||||
"<ul class="contains-task-list">
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-1" />unchecked todo item 1</li>
|
||||
<li>[ ]</li>
|
||||
<li>[ ] not a todo item 2</li>
|
||||
<li>[ x] not a todo item 3</li>
|
||||
<li>[x ] not a todo item 4</li>
|
||||
<li>[ x ] not a todo item 5</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" checked="" disabled="" id="task-item-7" />todo item 6</li>
|
||||
</ul>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`markdown-it-task-lists renders mixedNested correctly 1`] = `
|
||||
"<h1>Test 1</h1>
|
||||
<ol>
|
||||
<li>foo
|
||||
<ul class="contains-task-list">
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-4" />nested unchecked item 1</li>
|
||||
<li>not a todo item 2</li>
|
||||
<li>not a todo item 3</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" checked="" disabled="" id="task-item-7" />nested checked item 4</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>bar</li>
|
||||
<li>spam</li>
|
||||
</ol>
|
||||
<h1>Test 2</h1>
|
||||
<ul>
|
||||
<li>foo
|
||||
<ul class="contains-task-list">
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-14" />nested unchecked item 1</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-15" />nested unchecked item 2</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" checked="" disabled="" id="task-item-16" />nested checked item 3</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" checked="" disabled="" id="task-item-17" />nested checked item 4</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`markdown-it-task-lists renders ordered correctly 1`] = `
|
||||
"<ol class="contains-task-list">
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" checked="" disabled="" id="task-item-1" />checked ordered 1</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-2" />unchecked ordered 2</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" checked="" disabled="" id="task-item-3" />checked ordered 3</li>
|
||||
<li class="task-list-item "><input class="task-list-item-checkbox" type="checkbox" disabled="" id="task-item-4" />unchecked ordered 4</li>
|
||||
</ol>
|
||||
"
|
||||
`;
|
74
markdown-it-plugins/src/task-lists/index.test.ts
Normal file
74
markdown-it-plugins/src/task-lists/index.test.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it/lib'
|
||||
import { taskLists } from './index.js'
|
||||
import { describe, expect, it } from '@jest/globals'
|
||||
|
||||
describe('markdown-it-task-lists', () => {
|
||||
it('renders bullet correctly', () => {
|
||||
const taskListMarkdownParser = new MarkdownIt().use(taskLists)
|
||||
expect(
|
||||
taskListMarkdownParser.render(`
|
||||
- [ ] unchecked item 1
|
||||
- [ ] unchecked item 2
|
||||
- [ ] unchecked item 3
|
||||
- [x] checked item 4
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders dirty correctly', () => {
|
||||
const taskListMarkdownParser = new MarkdownIt().use(taskLists)
|
||||
expect(
|
||||
taskListMarkdownParser.render(`
|
||||
- [ ] unchecked todo item 1
|
||||
- [ ]
|
||||
- [ ] not a todo item 2
|
||||
- [ x] not a todo item 3
|
||||
- [x ] not a todo item 4
|
||||
- [ x ] not a todo item 5
|
||||
- [x] todo item 6
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders mixedNested correctly', () => {
|
||||
const taskListMarkdownParser = new MarkdownIt().use(taskLists)
|
||||
expect(
|
||||
taskListMarkdownParser.render(`
|
||||
# Test 1
|
||||
|
||||
1. foo
|
||||
* [ ] nested unchecked item 1
|
||||
* not a todo item 2
|
||||
* not a todo item 3
|
||||
* [x] nested checked item 4
|
||||
2. bar
|
||||
3. spam
|
||||
|
||||
# Test 2
|
||||
|
||||
- foo
|
||||
- [ ] nested unchecked item 1
|
||||
- [ ] nested unchecked item 2
|
||||
- [x] nested checked item 3
|
||||
- [X] nested checked item 4
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
it('renders ordered correctly', () => {
|
||||
const taskListMarkdownParser = new MarkdownIt().use(taskLists)
|
||||
expect(
|
||||
taskListMarkdownParser.render(`
|
||||
1. [x] checked ordered 1
|
||||
2. [ ] unchecked ordered 2
|
||||
3. [x] checked ordered 3
|
||||
4. [ ] unchecked ordered 4
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
})
|
159
markdown-it-plugins/src/task-lists/index.ts
Normal file
159
markdown-it-plugins/src/task-lists/index.ts
Normal file
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
// Markdown-it plugin to render GitHub-style task lists; see
|
||||
//
|
||||
// https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments
|
||||
// https://github.com/blog/1825-task-lists-in-all-markdown-documents
|
||||
|
||||
import MarkdownIt from 'markdown-it/lib'
|
||||
import StateCore from 'markdown-it/lib/rules_core/state_core.js'
|
||||
import Token from 'markdown-it/lib/token.js'
|
||||
|
||||
interface TaskListsOptions {
|
||||
enabled: boolean
|
||||
label: boolean
|
||||
lineNumber: boolean
|
||||
}
|
||||
|
||||
const checkboxRegex = /^ *\[([\sx])] /i
|
||||
|
||||
export function taskLists(
|
||||
md: MarkdownIt,
|
||||
options: TaskListsOptions = { enabled: false, label: false, lineNumber: false }
|
||||
): void {
|
||||
md.core.ruler.after('inline', 'task-lists', (state) => processToken(state, options))
|
||||
md.renderer.rules.taskListItemCheckbox = (tokens) => {
|
||||
const token = tokens[0]
|
||||
const checkedAttribute = token.attrGet('checked') ? 'checked="" ' : ''
|
||||
const disabledAttribute = token.attrGet('disabled') ? 'disabled="" ' : ''
|
||||
const line = token.attrGet('line')
|
||||
const idAttribute = `id="${token.attrGet('id')}" `
|
||||
const dataLineAttribute = line && options.lineNumber ? `data-line="${line}" ` : ''
|
||||
|
||||
return `<input class="task-list-item-checkbox" type="checkbox" ${checkedAttribute}${disabledAttribute}${dataLineAttribute}${idAttribute}/>`
|
||||
}
|
||||
|
||||
md.renderer.rules.taskListItemLabel_close = () => {
|
||||
return '</label>'
|
||||
}
|
||||
|
||||
md.renderer.rules.taskListItemLabel_open = (tokens: Token[]) => {
|
||||
const token = tokens[0]
|
||||
const id = token.attrGet('id')
|
||||
return `<label for="${id}">`
|
||||
}
|
||||
}
|
||||
|
||||
function processToken(state: StateCore, options: TaskListsOptions): boolean {
|
||||
const allTokens = state.tokens
|
||||
for (let i = 2; i < allTokens.length; i++) {
|
||||
if (!isTodoItem(allTokens, i)) {
|
||||
continue
|
||||
}
|
||||
|
||||
todoify(allTokens[i], options)
|
||||
allTokens[i - 2].attrJoin('class', `task-list-item ${options.enabled ? ' enabled' : ''}`)
|
||||
|
||||
const parentToken = findParentToken(allTokens, i - 2)
|
||||
if (parentToken) {
|
||||
const classes = parentToken.attrGet('class') ?? ''
|
||||
if (!classes.match(/(^| )contains-task-list/)) {
|
||||
parentToken.attrJoin('class', 'contains-task-list')
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function findParentToken(tokens: Token[], index: number): Token | undefined {
|
||||
const targetLevel = tokens[index].level - 1
|
||||
for (let currentTokenIndex = index - 1; currentTokenIndex >= 0; currentTokenIndex--) {
|
||||
if (tokens[currentTokenIndex].level === targetLevel) {
|
||||
return tokens[currentTokenIndex]
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function isTodoItem(tokens: Token[], index: number): boolean {
|
||||
return (
|
||||
isInline(tokens[index]) &&
|
||||
isParagraph(tokens[index - 1]) &&
|
||||
isListItem(tokens[index - 2]) &&
|
||||
startsWithTodoMarkdown(tokens[index])
|
||||
)
|
||||
}
|
||||
|
||||
function todoify(token: Token, options: TaskListsOptions): void {
|
||||
if (token.children == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const id = generateIdForToken(token)
|
||||
|
||||
token.children.splice(0, 0, createCheckboxToken(token, options.enabled, id))
|
||||
token.children[1].content = token.children[1].content.replace(checkboxRegex, '')
|
||||
|
||||
if (options.label) {
|
||||
token.children.splice(1, 0, createLabelBeginToken(id))
|
||||
token.children.push(createLabelEndToken())
|
||||
}
|
||||
}
|
||||
|
||||
function generateIdForToken(token: Token): string {
|
||||
if (token.map) {
|
||||
return `task-item-${token.map[0]}`
|
||||
} else {
|
||||
return `task-item-${Math.ceil(Math.random() * (10000 * 1000) - 1000)}`
|
||||
}
|
||||
}
|
||||
|
||||
function createCheckboxToken(token: Token, enabled: boolean, id: string): Token {
|
||||
const checkbox = new Token('taskListItemCheckbox', '', 0)
|
||||
if (!enabled) {
|
||||
checkbox.attrSet('disabled', 'true')
|
||||
}
|
||||
if (token.map) {
|
||||
checkbox.attrSet('line', token.map[0].toString())
|
||||
}
|
||||
|
||||
checkbox.attrSet('id', id)
|
||||
|
||||
const checkboxRegexResult = checkboxRegex.exec(token.content)
|
||||
const isChecked = checkboxRegexResult?.[1].toLowerCase() === 'x'
|
||||
if (isChecked) {
|
||||
checkbox.attrSet('checked', 'true')
|
||||
}
|
||||
|
||||
return checkbox
|
||||
}
|
||||
|
||||
function createLabelBeginToken(id: string): Token {
|
||||
const labelBeginToken = new Token('taskListItemLabel_open', '', 1)
|
||||
labelBeginToken.attrSet('id', id)
|
||||
return labelBeginToken
|
||||
}
|
||||
|
||||
function createLabelEndToken(): Token {
|
||||
return new Token('taskListItemLabel_close', '', -1)
|
||||
}
|
||||
|
||||
function isInline(token: Token): boolean {
|
||||
return token.type === 'inline'
|
||||
}
|
||||
|
||||
function isParagraph(token: Token): boolean {
|
||||
return token.type === 'paragraph_open'
|
||||
}
|
||||
|
||||
function isListItem(token: Token): boolean {
|
||||
return token.type === 'list_item_open'
|
||||
}
|
||||
|
||||
function startsWithTodoMarkdown(token: Token): boolean {
|
||||
return checkboxRegex.test(token.content)
|
||||
}
|
95
markdown-it-plugins/src/toc/__snapshots__/index.test.ts.snap
Normal file
95
markdown-it-plugins/src/toc/__snapshots__/index.test.ts.snap
Normal file
|
@ -0,0 +1,95 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
/*
|
||||
* SPDX-FileCopyrightText: Original: (c) 2018 Fabio Zendhi Nagao / Modifications: (c) 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
exports[`toc ignores the levels in the placeholder if not sorted 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-1">Head 1</a><ol><li><a href="#head-2">Head 2</a><ol><li><a href="#head-3">Head 3</a><ol><li><a href="#head-4">Head 4</a></li></ol></li></ol></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Head 2</h2>
|
||||
<h3>Head 3</h3>
|
||||
<h4>Head 4</h4>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with a level number array 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-2">Head 2</a><ol><li><a href="#head-4">Head 4</a></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Head 2</h2>
|
||||
<h3>Head 3</h3>
|
||||
<h4>Head 4</h4>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with a single level number 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-2">Head 2</a><ol><li><a href="#head-3">Head 3</a></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Head 2</h2>
|
||||
<h3>Head 3</h3>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with custom allowed token types 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#text"> text</a></li><li><a href="#head-2">Head 2</a></li></ol></nav><h1><code>Head 1</code> text</h1>
|
||||
<h1>Head 2</h1>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with custom classes 1`] = `
|
||||
"<nav id="containerId" class="containerClass"><ol class="listClass"><li class="itemClass"><a class="linkClass" href="#head-1">Head 1</a><ol class="listClass"><li class="itemClass"><a class="linkClass" href="#heading-2">Heading 2</a><ol class="listClass"><li class="itemClass"><a class="linkClass" href="#heading-3">Heading 3</a></li></ol></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with custom format function 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-1">HEAD 1</a><ol><li><a href="#heading-2">HEADING 2</a><ol><li><a href="#heading-3">HEADING 3</a></li></ol></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with custom slugify 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#slug-Head 1-0">Head 1</a><ol><li><a href="#slug-Heading 2-0">Heading 2</a><ol><li><a href="#slug-Heading 3-0">Heading 3</a></li></ol></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with custom unique slug start index 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-1-10">Head 1</a><ol><li><a href="#heading-2-10">Heading 2</a><ol><li><a href="#heading-3-10">Heading 3</a></li></ol></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with default settings 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-1">Head 1</a><ol><li><a href="#heading-2">Heading 2</a><ol><li><a href="#heading-3">Heading 3</a></li></ol></li></ol></li><li><a href="#head-1-1">Head 1</a></li><li><a href="#head-1-2">Head 1</a></li><li><a href="#head-1-3">Head 1</a></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
<h1>Head 1</h1>
|
||||
<h1>Head 1</h1>
|
||||
<h1>Head 1</h1>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with levels in the placeholder 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-2">Head 2</a><ol><li><a href="#head-3">Head 3</a><ol></ol></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Head 2</h2>
|
||||
<h3>Head 3</h3>
|
||||
<h4>Head 4</h4>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with ordered list 1`] = `
|
||||
"<nav class="table-of-contents"><ol><li><a href="#head-1">Head 1</a><ol><li><a href="#heading-2">Heading 2</a><ol><li><a href="#heading-3">Heading 3</a></li></ol></li></ol></li></ol></nav><h1>Head 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`toc renders a toc with unordered list 1`] = `
|
||||
"<nav class="table-of-contents"><ul><li><a href="#head-1">Head 1</a><ul><li><a href="#heading-2">Heading 2</a><ul><li><a href="#heading-3">Heading 3</a></li></ul></li></ul></li></ul></nav><h1>Head 1</h1>
|
||||
<h2>Heading 2</h2>
|
||||
<h3>Heading 3</h3>
|
||||
"
|
||||
`;
|
186
markdown-it-plugins/src/toc/index.test.ts
Normal file
186
markdown-it-plugins/src/toc/index.test.ts
Normal file
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import MarkdownIt from 'markdown-it/lib'
|
||||
import { toc } from './plugin.js'
|
||||
import { describe, expect, it, jest } from '@jest/globals'
|
||||
|
||||
describe('toc', () => {
|
||||
const simpleContent = `
|
||||
[toc]
|
||||
|
||||
# Head 1
|
||||
|
||||
## Heading 2
|
||||
|
||||
### Heading 3
|
||||
`
|
||||
|
||||
it('renders a toc with default settings', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc)
|
||||
expect(
|
||||
markdownIt.render(`
|
||||
[toc]
|
||||
|
||||
# Head 1
|
||||
|
||||
## Heading 2
|
||||
|
||||
### Heading 3
|
||||
|
||||
# Head 1
|
||||
|
||||
# Head 1
|
||||
|
||||
# Head 1
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with custom slugify', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { slugify: (slug, index) => `slug-${slug}-${index}` })
|
||||
expect(markdownIt.render(simpleContent)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with custom unique slug start index', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { uniqueSlugStartIndex: 10 })
|
||||
expect(markdownIt.render(simpleContent)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with custom classes', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, {
|
||||
containerClass: 'containerClass',
|
||||
listClass: 'listClass',
|
||||
itemClass: 'itemClass',
|
||||
linkClass: 'linkClass',
|
||||
containerId: 'containerId'
|
||||
})
|
||||
expect(markdownIt.render(simpleContent)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with a single level number', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { level: 2 })
|
||||
expect(
|
||||
markdownIt.render(`
|
||||
[toc]
|
||||
|
||||
# Head 1
|
||||
|
||||
## Head 2
|
||||
|
||||
### Head 3
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with a level number array', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { level: [2, 4] })
|
||||
expect(
|
||||
markdownIt.render(`
|
||||
[toc]
|
||||
|
||||
# Head 1
|
||||
|
||||
## Head 2
|
||||
|
||||
### Head 3
|
||||
|
||||
#### Head 4
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with levels in the placeholder', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc)
|
||||
expect(
|
||||
markdownIt.render(`
|
||||
[toc:2:3]
|
||||
|
||||
# Head 1
|
||||
|
||||
## Head 2
|
||||
|
||||
### Head 3
|
||||
|
||||
#### Head 4
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('ignores the levels in the placeholder if not sorted', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc)
|
||||
expect(
|
||||
markdownIt.render(`
|
||||
[toc:3:2]
|
||||
|
||||
# Head 1
|
||||
|
||||
## Head 2
|
||||
|
||||
### Head 3
|
||||
|
||||
#### Head 4
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with ordered list', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { listType: 'ol' })
|
||||
expect(markdownIt.render(simpleContent)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with unordered list', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { listType: 'ul' })
|
||||
expect(markdownIt.render(simpleContent)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc with custom format function', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { format: (name) => name.toUpperCase() })
|
||||
expect(markdownIt.render(simpleContent)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders a toc and executes the callback', () => {
|
||||
const callback = jest.fn()
|
||||
const markdownIt = new MarkdownIt().use(toc, { callback })
|
||||
markdownIt.render(simpleContent)
|
||||
expect(callback).toBeCalledWith({
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
children: [],
|
||||
level: 3,
|
||||
name: 'Heading 3'
|
||||
}
|
||||
],
|
||||
level: 2,
|
||||
name: 'Heading 2'
|
||||
}
|
||||
],
|
||||
level: 1,
|
||||
name: 'Head 1'
|
||||
}
|
||||
],
|
||||
level: 0,
|
||||
name: ''
|
||||
})
|
||||
})
|
||||
|
||||
it('renders a toc with custom allowed token types', () => {
|
||||
const markdownIt = new MarkdownIt().use(toc, { allowedTokenTypes: ['text'] })
|
||||
expect(
|
||||
markdownIt.render(`
|
||||
[toc]
|
||||
|
||||
# \`Head 1\` text
|
||||
|
||||
# Head 2
|
||||
`)
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
})
|
10
markdown-it-plugins/src/toc/index.ts
Normal file
10
markdown-it-plugins/src/toc/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export { toc } from './plugin.js'
|
||||
export { defaultOptions } from './toc-options.js'
|
||||
export type { TocAst } from './toc-ast.js'
|
||||
export type { TocOptions } from './toc-options.js'
|
157
markdown-it-plugins/src/toc/plugin.ts
Normal file
157
markdown-it-plugins/src/toc/plugin.ts
Normal file
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: Original: (c) 2018 Fabio Zendhi Nagao / Modifications: (c) 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { Optional } from '@mrdrogdrog/optional'
|
||||
import { encode as htmlencode } from 'html-entities'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import StateBlock from 'markdown-it/lib/rules_block/state_block.js'
|
||||
import Token from 'markdown-it/lib/token.js'
|
||||
import { TocAst } from './toc-ast.js'
|
||||
import { renderAstToHtml } from './toc-body-renderer.js'
|
||||
import { defaultOptions, TocOptions } from './toc-options.js'
|
||||
|
||||
class Plugin {
|
||||
private readonly tocOptions: TocOptions
|
||||
private currentAst?: TocAst
|
||||
public readonly START_LEVEL_ATTRIBUTE_NAME = 'startLevel'
|
||||
public readonly END_LEVEL_ATTRIBUTE_NAME = 'endLevel'
|
||||
|
||||
private readonly TOC_PLACEHOLDER = /^\[\[toc(?::(\d+):(\d+))?]]|\[toc(?::(\d+):(\d+))?]$/i
|
||||
|
||||
public constructor(md: MarkdownIt, tocOptions?: Partial<TocOptions>) {
|
||||
this.tocOptions = {
|
||||
...defaultOptions,
|
||||
...tocOptions
|
||||
}
|
||||
md.renderer.rules.tocOpen = this.renderTocOpen.bind(this)
|
||||
md.renderer.rules.tocClose = this.renderTocClose.bind(this)
|
||||
md.renderer.rules.tocBody = this.renderTocBody.bind(this)
|
||||
md.core.ruler.push('generateTocAst', (state) => this.generateTocAst(state.tokens))
|
||||
md.block.ruler.before('heading', 'toc', this.generateTocToken.bind(this), {
|
||||
alt: ['paragraph', 'reference', 'blockquote']
|
||||
})
|
||||
}
|
||||
|
||||
private generateTocToken(state: StateBlock, startLine: number, _endLine: number, silent: boolean): boolean {
|
||||
const pos = state.bMarks[startLine] + state.tShift[startLine]
|
||||
const max = state.eMarks[startLine]
|
||||
|
||||
// use whitespace as a line tokenizer and extract the first token
|
||||
// to test against the placeholder anchored pattern, rejecting if false
|
||||
const lineFirstToken = state.src.slice(pos, max).split(' ')[0]
|
||||
|
||||
const matches = this.TOC_PLACEHOLDER.exec(lineFirstToken)
|
||||
if (matches === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (silent) {
|
||||
return true
|
||||
}
|
||||
|
||||
state.line = startLine + 1
|
||||
const tocOpenToken = state.push('tocOpen', 'nav', 1)
|
||||
tocOpenToken.markup = ''
|
||||
tocOpenToken.map = [startLine, state.line]
|
||||
|
||||
const tocBodyToken = state.push('tocBody', '', 0)
|
||||
tocBodyToken.markup = ''
|
||||
tocBodyToken.map = [startLine, state.line]
|
||||
tocBodyToken.children = []
|
||||
|
||||
const startLevel = matches[3]
|
||||
const endLevel = matches[4]
|
||||
if (startLevel !== undefined && endLevel !== undefined) {
|
||||
tocBodyToken.attrSet(this.START_LEVEL_ATTRIBUTE_NAME, startLevel)
|
||||
tocBodyToken.attrSet(this.END_LEVEL_ATTRIBUTE_NAME, endLevel)
|
||||
}
|
||||
|
||||
const tocCloseToken = state.push('tocClose', 'nav', -1)
|
||||
tocCloseToken.markup = ''
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private generateTocAst(tokens: Token[]) {
|
||||
this.currentAst = this.headings2ast(tokens)
|
||||
this.tocOptions.callback?.(this.currentAst)
|
||||
}
|
||||
|
||||
private renderTocOpen(): string {
|
||||
const id = this.tocOptions.containerId ? ` id="${htmlencode(this.tocOptions.containerId)}"` : ''
|
||||
return `<nav${id} class="${htmlencode(this.tocOptions.containerClass)}">`
|
||||
}
|
||||
|
||||
private renderTocClose(): string {
|
||||
return '</nav>'
|
||||
}
|
||||
|
||||
private createNumberRangeArray(from: number, to: number): number[] {
|
||||
return Array.from(Array(to - from + 1).keys()).map((value) => value + from)
|
||||
}
|
||||
|
||||
private renderTocBody(tokens: Token[], index: number): string {
|
||||
const bodyToken = tokens[index]
|
||||
const startLevel = Optional.ofNullable(bodyToken?.attrGet(this.START_LEVEL_ATTRIBUTE_NAME))
|
||||
.map(parseInt)
|
||||
.filter(isFinite)
|
||||
.orElse(null)
|
||||
const endLevel = Optional.ofNullable(bodyToken?.attrGet(this.END_LEVEL_ATTRIBUTE_NAME))
|
||||
.map(parseInt)
|
||||
.filter(isFinite)
|
||||
.orElse(null)
|
||||
|
||||
const modifiedTocOptions =
|
||||
startLevel !== null && endLevel !== null && startLevel <= endLevel
|
||||
? { ...this.tocOptions, level: this.createNumberRangeArray(startLevel, endLevel) }
|
||||
: this.tocOptions
|
||||
|
||||
return this.currentAst ? renderAstToHtml(this.currentAst, modifiedTocOptions) : ''
|
||||
}
|
||||
|
||||
private headings2ast(tokens: Token[]): TocAst {
|
||||
const ast: TocAst = { level: 0, name: '', children: [] }
|
||||
const stack = [ast]
|
||||
|
||||
for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex++) {
|
||||
const token = tokens[tokenIndex]
|
||||
if (token.type !== 'heading_open') {
|
||||
continue
|
||||
}
|
||||
const nextToken = tokens[tokenIndex + 1]
|
||||
const key = (nextToken?.children ?? [])
|
||||
.filter((token) => this.tocOptions.allowedTokenTypes.includes(token.type))
|
||||
.reduce((s, t) => s + t.content, '')
|
||||
|
||||
const node: TocAst = {
|
||||
level: parseInt(token.tag.slice(1), 10),
|
||||
name: key,
|
||||
children: []
|
||||
}
|
||||
if (node.level > stack[0].level) {
|
||||
stack[0].children.push(node)
|
||||
stack.unshift(node)
|
||||
} else if (node.level === stack[0].level) {
|
||||
stack[1].children.push(node)
|
||||
stack[0] = node
|
||||
} else {
|
||||
while (node.level <= stack[0].level) stack.shift()
|
||||
stack[0].children.push(node)
|
||||
stack.unshift(node)
|
||||
}
|
||||
}
|
||||
|
||||
return ast
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new TOC plugin.
|
||||
*
|
||||
* @param md The markdown-it instance that should be configured
|
||||
* @param options The additional options that configure the plugin
|
||||
*/
|
||||
export const toc: MarkdownIt.PluginWithOptions<Partial<TocOptions>> = (md, options) => new Plugin(md, options)
|
11
markdown-it-plugins/src/toc/toc-ast.ts
Normal file
11
markdown-it-plugins/src/toc/toc-ast.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
export interface TocAst {
|
||||
level: number
|
||||
name: string
|
||||
children: TocAst[]
|
||||
}
|
63
markdown-it-plugins/src/toc/toc-body-renderer.ts
Normal file
63
markdown-it-plugins/src/toc/toc-body-renderer.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { encode as htmlencode } from 'html-entities'
|
||||
import { TocAst } from './toc-ast.js'
|
||||
import { TocOptions } from './toc-options.js'
|
||||
|
||||
/**
|
||||
* Renders an HTML listing of the given tree.
|
||||
*
|
||||
* @param tree The tree that should be represented as HTML tree
|
||||
* @param tocOptions additional options that configure the rendering
|
||||
*/
|
||||
export function renderAstToHtml(tree: TocAst, tocOptions: TocOptions): string {
|
||||
if (tree.children.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
let buffer = ''
|
||||
const tag = htmlencode(tocOptions.listType)
|
||||
if (tree.level === 0 || isLevelSelected(tree.level, tocOptions.level)) {
|
||||
const listClass = tocOptions.listClass !== '' ? ` class="${htmlencode(tocOptions.listClass)}"` : ''
|
||||
buffer += `<${tag + listClass}>`
|
||||
}
|
||||
const usedSlugs: string[] = []
|
||||
const parts = tree.children.map((node) => {
|
||||
const subNodesHtml = renderAstToHtml(node, tocOptions)
|
||||
if (isLevelSelected(node.level, tocOptions.level)) {
|
||||
const anchorContent = htmlencode(tocOptions.format?.(node.name) ?? node.name)
|
||||
const anchorId = generateUniqueSlug(node.name, tocOptions, usedSlugs)
|
||||
usedSlugs.push(anchorId)
|
||||
const itemClass = tocOptions.itemClass !== '' ? ` class="${htmlencode(tocOptions.itemClass)}"` : ''
|
||||
const linkClass = tocOptions.linkClass !== '' ? ` class="${htmlencode(tocOptions.linkClass)}"` : ''
|
||||
return `<li${itemClass}><a${linkClass} href="#${anchorId}">${anchorContent}</a>${subNodesHtml}</li>`
|
||||
} else {
|
||||
return subNodesHtml
|
||||
}
|
||||
})
|
||||
buffer += parts.join('')
|
||||
if (tree.level === 0 || isLevelSelected(tree.level, tocOptions.level)) {
|
||||
buffer += `</${tag}>`
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
function isLevelSelected(level: number, levels: number | number[]): boolean {
|
||||
return Array.isArray(levels) ? levels.includes(level) : level >= levels
|
||||
}
|
||||
|
||||
function generateUniqueSlug(slug: string, tocOptions: TocOptions, usedSlugs: string[]): string {
|
||||
for (let index = tocOptions.uniqueSlugStartIndex; index < Number.MAX_VALUE; index += 1) {
|
||||
const slugCandidate: string = tocOptions.slugify(slug, index)
|
||||
const slugWithIndex = index === 0 ? slugCandidate : `${slugCandidate}-${index}`
|
||||
|
||||
if (!usedSlugs.includes(slugWithIndex)) {
|
||||
return slugWithIndex
|
||||
}
|
||||
}
|
||||
throw new Error('Too many slug with same name')
|
||||
}
|
42
markdown-it-plugins/src/toc/toc-options.ts
Normal file
42
markdown-it-plugins/src/toc/toc-options.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { TocAst } from './toc-ast.js'
|
||||
|
||||
export type TocOptions = {
|
||||
slugify: (name: string, index: number) => string
|
||||
uniqueSlugStartIndex: number
|
||||
containerClass: string
|
||||
containerId: string
|
||||
listClass: string
|
||||
itemClass: string
|
||||
linkClass: string
|
||||
level: number | number[]
|
||||
listType: 'ol' | 'ul'
|
||||
format?: (name: string) => string
|
||||
callback?: (ast: TocAst) => void
|
||||
allowedTokenTypes: string[]
|
||||
}
|
||||
|
||||
function defaultSlugify(name: string) {
|
||||
return encodeURIComponent(String(name).trim().toLowerCase().replace(/\s+/g, '-'))
|
||||
}
|
||||
|
||||
/**
|
||||
* The default options for the toc plugin.
|
||||
*/
|
||||
export const defaultOptions: TocOptions = {
|
||||
uniqueSlugStartIndex: 0,
|
||||
containerClass: 'table-of-contents',
|
||||
containerId: '',
|
||||
listClass: '',
|
||||
itemClass: '',
|
||||
linkClass: '',
|
||||
level: 1,
|
||||
listType: 'ol',
|
||||
allowedTokenTypes: ['text', 'code_inline'],
|
||||
slugify: defaultSlugify
|
||||
}
|
22
markdown-it-plugins/tsconfig.base.json
Normal file
22
markdown-it-plugins/tsconfig.base.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"removeComments": true,
|
||||
"preserveConstEnums": true,
|
||||
"lib": [
|
||||
"es2022",
|
||||
"dom"
|
||||
],
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"../dist", "**/*.test.ts"]
|
||||
}
|
3
markdown-it-plugins/tsconfig.base.json.license
Normal file
3
markdown-it-plugins/tsconfig.base.json.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2023 Tilman Vatteroth
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
10
markdown-it-plugins/tsconfig.cjs.json
Normal file
10
markdown-it-plugins/tsconfig.cjs.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends" : "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"target": "ES2015",
|
||||
"outDir": "../dist/cjs",
|
||||
"declarationDir": "../dist/cjs",
|
||||
"moduleResolution": "node"
|
||||
}
|
||||
}
|
3
markdown-it-plugins/tsconfig.cjs.json.license
Normal file
3
markdown-it-plugins/tsconfig.cjs.json.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2023 Tilman Vatteroth
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
10
markdown-it-plugins/tsconfig.esm.json
Normal file
10
markdown-it-plugins/tsconfig.esm.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends" : "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"target" : "esnext",
|
||||
"outDir": "../dist/esm",
|
||||
"moduleResolution": "NodeNext",
|
||||
"declarationDir": "../dist/esm"
|
||||
}
|
||||
}
|
3
markdown-it-plugins/tsconfig.esm.json.license
Normal file
3
markdown-it-plugins/tsconfig.esm.json.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2023 Tilman Vatteroth
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
6
markdown-it-plugins/tsconfig.test.json
Normal file
6
markdown-it-plugins/tsconfig.test.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends" : "./tsconfig.esm.json",
|
||||
"exclude": [
|
||||
"../dist"
|
||||
]
|
||||
}
|
3
markdown-it-plugins/tsconfig.test.json.license
Normal file
3
markdown-it-plugins/tsconfig.test.json.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2023 Tilman Vatteroth
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
Loading…
Reference in a new issue