add-cms
This commit is contained in:
562
source/admin/packages/decap-cms-widget-markdown/CHANGELOG.md
Normal file
562
source/admin/packages/decap-cms-widget-markdown/CHANGELOG.md
Normal file
@@ -0,0 +1,562 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.5.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.4.1...decap-cms-widget-markdown@3.5.0) (2025-07-15)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [3.4.1](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.4.0...decap-cms-widget-markdown@3.4.1) (2025-07-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
# [3.4.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.3.0...decap-cms-widget-markdown@3.4.0) (2025-06-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **#7375:** fix dependency issues ([#7394](https://github.com/decaporg/decap-cms/issues/7394)) ([871ee26](https://github.com/decaporg/decap-cms/commit/871ee2653bb26b500efb39b6862e8d3681ec0338))
|
||||
|
||||
# [3.3.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.2.0...decap-cms-widget-markdown@3.3.0) (2025-01-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **markdown:** convert inline CSS from Google Docs to Markdown ([#7351](https://github.com/decaporg/decap-cms/issues/7351)) ([8b8e873](https://github.com/decaporg/decap-cms/commit/8b8e873af9a0749720ec03cadbc4b0d391ad84e1))
|
||||
|
||||
### Features
|
||||
|
||||
- visual editing (click-to-edit) ([#7374](https://github.com/decaporg/decap-cms/issues/7374)) ([989c2dd](https://github.com/decaporg/decap-cms/commit/989c2dd6ed80f69b572b8b73c4e37b5106ae04fb))
|
||||
|
||||
# [3.2.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.1.6...decap-cms-widget-markdown@3.2.0) (2024-11-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **markdown:** copying html into markdown ([#7290](https://github.com/decaporg/decap-cms/issues/7290)) ([f6959e2](https://github.com/decaporg/decap-cms/commit/f6959e2b5983a1d9ad8a3ad4f8d16d9f7a9e5225))
|
||||
|
||||
## [3.1.6](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.1.5...decap-cms-widget-markdown@3.1.6) (2024-08-13)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "Update dependencies (#7264)" ([22d483a](https://github.com/decaporg/decap-cms/commit/22d483a5b0c654071ae05735ac4f49abdc13d38c)), closes [#7264](https://github.com/decaporg/decap-cms/issues/7264)
|
||||
|
||||
## [3.1.5](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.1.4...decap-cms-widget-markdown@3.1.5) (2024-08-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [3.1.4](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.1.3...decap-cms-widget-markdown@3.1.4) (2024-08-07)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [3.1.3](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.1.2...decap-cms-widget-markdown@3.1.3) (2024-03-28)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [3.1.2](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.1.1...decap-cms-widget-markdown@3.1.2) (2024-03-21)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [3.1.1](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.1.0-beta.2...decap-cms-widget-markdown@3.1.1) (2024-03-08)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
# [3.1.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.1.0-beta.2...decap-cms-widget-markdown@3.1.0) (2024-02-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
# [3.1.0-beta.2](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.1.0-beta.1...decap-cms-widget-markdown@3.1.0-beta.2) (2024-01-31)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
# [3.1.0-beta.1](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.1.0-beta.0...decap-cms-widget-markdown@3.1.0-beta.1) (2024-01-16)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
# [3.1.0-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.1.0...decap-cms-widget-markdown@3.1.0-beta.0) (2023-10-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([b89fc89](https://github.com/decaporg/decap-cms/commit/b89fc894dfbb5f4136b2e5427fd25a29378a58c6))
|
||||
|
||||
## [3.0.3](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.0.2...decap-cms-widget-markdown@3.0.3) (2023-10-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [3.0.2](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.0.1...decap-cms-widget-markdown@3.0.2) (2023-10-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [3.0.1](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@3.0.0...decap-cms-widget-markdown@3.0.1) (2023-08-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update peer dependencies ([#6886](https://github.com/decaporg/decap-cms/issues/6886)) ([e580ce5](https://github.com/decaporg/decap-cms/commit/e580ce52ce5f80fa040e8fbcab7fed0744f4f695))
|
||||
|
||||
# [3.0.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@2.16.0...decap-cms-widget-markdown@3.0.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
# [2.16.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@2.16.0-beta.0...decap-cms-widget-markdown@2.16.0) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
# 2.16.0-beta.0 (2023-08-18)
|
||||
|
||||
### Features
|
||||
|
||||
- rename packages ([#6863](https://github.com/decaporg/decap-cms/issues/6863)) ([d515e7b](https://github.com/decaporg/decap-cms/commit/d515e7bd33216a775d96887b08c4f7b1962941bb))
|
||||
|
||||
## [2.15.2-beta.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@2.15.1...decap-cms-widget-markdown@2.15.2-beta.0) (2023-07-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- accessibility - color contrast and keyboard navigation ([#6757](https://github.com/decaporg/decap-cms/issues/6757)) ([194d1ce](https://github.com/decaporg/decap-cms/commit/194d1ce351c35d3feed4c1be704fbf79ac3c0efa))
|
||||
|
||||
## [2.15.1](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@2.15.0...decap-cms-widget-markdown@2.15.1) (2022-04-13)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
# [2.15.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@2.14.4...decap-cms-widget-markdown@2.15.0) (2022-01-21)
|
||||
|
||||
### Features
|
||||
|
||||
- use keyboard shortcuts to insert bullet points ([#6134](https://github.com/decaporg/decap-cms/issues/6134)) ([dd149f6](https://github.com/decaporg/decap-cms/commit/dd149f6d0479d20eef0bc9cede738784c9cdb4fd))
|
||||
|
||||
## [2.14.4](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@2.14.3...decap-cms-widget-markdown@2.14.4) (2021-10-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **richtext:** improvement to the Rich Text Editor [#5446](https://github.com/decaporg/decap-cms/issues/5446) ([#5897](https://github.com/decaporg/decap-cms/issues/5897)) ([06c7e25](https://github.com/decaporg/decap-cms/commit/06c7e251ce5e97a4949aa308070c3a659b50c780))
|
||||
|
||||
## [2.14.3](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@2.14.2...decap-cms-widget-markdown@2.14.3) (2021-10-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- dropdown overflow in markdown widget ([#5879](https://github.com/decaporg/decap-cms/issues/5879)) ([3ec1611](https://github.com/decaporg/decap-cms/commit/3ec161122d13e01dacb65fa9c109be9612b54d47))
|
||||
|
||||
## [2.14.2](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@2.14.1...decap-cms-widget-markdown@2.14.2) (2021-08-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **markdown-widget:** support arbitrary component order ([#5597](https://github.com/decaporg/decap-cms/issues/5597)) ([fbfab7c](https://github.com/decaporg/decap-cms/commit/fbfab7cda54aba68c948188d0ad5660431d275fc))
|
||||
|
||||
## [2.14.1](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@2.14.0...decap-cms-widget-markdown@2.14.1) (2021-08-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **markdown-widget:** apply list item style on each block in a selection ([#5676](https://github.com/decaporg/decap-cms/issues/5676)) ([04e5305](https://github.com/decaporg/decap-cms/commit/04e53054ceba8e2b6f3a2e7f1de5ecc3abe8431a))
|
||||
|
||||
# [2.14.0](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@2.13.6...decap-cms-widget-markdown@2.14.0) (2021-07-25)
|
||||
|
||||
### Features
|
||||
|
||||
- **widget-markdown:** allow registering remark plugins ([#5633](https://github.com/decaporg/decap-cms/issues/5633)) ([437f4bc](https://github.com/decaporg/decap-cms/commit/437f4bc634c5a52758bd06ab1709f2e66a71dce7))
|
||||
|
||||
## [2.13.6](https://github.com/decaporg/decap-cms/compare/decap-cms-widget-markdown@2.13.5...decap-cms-widget-markdown@2.13.6) (2021-07-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **widget-markdown:** Hitting Enter key in a list item doesn't create a new list item ([#5550](https://github.com/decaporg/decap-cms/issues/5550)) ([ab3e8e1](https://github.com/decaporg/decap-cms/commit/ab3e8e1f5a5fecd343e32cc31912ec912449e713))
|
||||
|
||||
## [2.13.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.13.4...decap-cms-widget-markdown@2.13.5) (2021-06-24)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [2.13.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.13.3...decap-cms-widget-markdown@2.13.4) (2021-06-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [2.13.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.13.2...decap-cms-widget-markdown@2.13.3) (2021-05-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **widget-markdown:** fix quote block and list highlighting ([#5422](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/5422)) ([b9624fc](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/b9624fc67376cd6c6850f9b1adbfa5c80f2a0ac0))
|
||||
|
||||
## [2.13.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.13.1...decap-cms-widget-markdown@2.13.2) (2021-05-19)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [2.13.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.13.0...decap-cms-widget-markdown@2.13.1) (2021-05-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update react-select to v3 ([#5394](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/5394)) ([03be13c](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/03be13c1e87b318fd10ae6f6ab54cd2634fb9662))
|
||||
|
||||
# [2.13.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.12.12...decap-cms-widget-markdown@2.13.0) (2021-05-04)
|
||||
|
||||
### Features
|
||||
|
||||
- added react 17 as peer dependency in packages ([#5316](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/5316)) ([9e42380](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/9e423805707321396eec137f5b732a5b07a0dd3f))
|
||||
|
||||
## [2.12.12](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.12.11...decap-cms-widget-markdown@2.12.12) (2021-04-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **widget-markdown:** improve UX in Markdown editor - link editing and selected heading underline ([#5104](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/5104)) ([dde1a9d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/dde1a9db5483912f626f13239d7a3d06d6c4e05c))
|
||||
|
||||
## [2.12.11](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.12.10...decap-cms-widget-markdown@2.12.11) (2021-02-23)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [2.12.10](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.12.9...decap-cms-widget-markdown@2.12.10) (2021-02-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **widget-markdown:** set toolbar item dropdown width to 'max-content' ([ecbf82e](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/ecbf82e961217869a2354e520cd7ccbfa8151c18))
|
||||
|
||||
## [2.12.9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.12.8...decap-cms-widget-markdown@2.12.9) (2021-02-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **security-markdown-widget:** allow sanitization of preview content ([#4886](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/4886)) ([27aec85](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/27aec85550c0be52c55f7b33314ecd52727fdcb5))
|
||||
|
||||
## [2.12.8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.12.7...decap-cms-widget-markdown@2.12.8) (2020-11-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update dependency is-hotkey to ^0.2.0 ([#4652](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/4652)) ([7828e15](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/7828e15453e55b5201f23b58817d73ecbd30a912))
|
||||
|
||||
## [2.12.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.12.6...decap-cms-widget-markdown@2.12.7) (2020-10-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **locale:** remove hard coded strings ([#4432](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/4432)) ([a5750d7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/a5750d782e9b4f0060362459037086f4d2f18acf))
|
||||
|
||||
## [2.12.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.12.5...decap-cms-widget-markdown@2.12.6) (2020-09-20)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [2.12.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.12.4...decap-cms-widget-markdown@2.12.5) (2020-09-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update dependency re-resizable to v6 ([#4308](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/4308)) ([de068cb](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/de068cba1d44ec76e47e28d724427a9f4a53e0fd))
|
||||
|
||||
## 2.12.4 (2020-09-08)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([828bb16](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/828bb16415b8c22a34caa19c50c38b24ffe9ceae))
|
||||
|
||||
## 2.12.3 (2020-08-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([8262487](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/82624879ccbcb16610090041db28f00714d924c8))
|
||||
|
||||
## 2.12.2 (2020-07-27)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([118d50a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/118d50a7a70295f25073e564b5161aa2b9883056))
|
||||
|
||||
## [2.12.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.12.0...decap-cms-widget-markdown@2.12.1) (2020-07-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **prop-types:** check for react components via PropTypes.elementType ([#4025](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/4025)) ([d3831b1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/d3831b1ed44fcff51a63f6645a5aa68332467dab))
|
||||
|
||||
# [2.12.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.11.3...decap-cms-widget-markdown@2.12.0) (2020-06-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update dependency react-monaco-editor to ^0.36.0 ([#3871](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3871)) ([dc429f8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/dc429f8ffa40bc6d5f024823a10ae99a49aebdb5))
|
||||
- **widget-markdown:** don't strip new lines from text nodes ([#3813](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3813)) ([7bc75d0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/7bc75d095bcdae0a85d95ed9d0c9188a89136805))
|
||||
- **widget-markdown:** headings dropdown not showing properly no firefox ([#3903](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3903)) ([2b01e00](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/2b01e009c69ecb932815eda69385703e5774d775))
|
||||
- update rehype-remark ([#3864](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3864)) ([53cba02](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/53cba022442ee2e996a8917fced57a311fe22da0))
|
||||
|
||||
### Features
|
||||
|
||||
- add widgets schema validation ([#3841](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3841)) ([2b46608](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/2b46608f86d22c8ad34f75e396be7c34462d9e99))
|
||||
|
||||
## [2.11.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.11.2...decap-cms-widget-markdown@2.11.3) (2020-05-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **deps:** update dependency rehype-stringify to v7 ([#3729](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3729)) ([a33aebb](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/a33aebbc58e345b3659a6fd93de9f9e755e57525))
|
||||
|
||||
## [2.11.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.11.1...decap-cms-widget-markdown@2.11.2) (2020-05-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- prevent escaping of footnotes and references ([#3646](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3646)) ([028ab53](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/028ab535df3e840bdf75c083ca7fbb275e0c61b3))
|
||||
|
||||
## [2.11.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.11.0...decap-cms-widget-markdown@2.11.1) (2020-04-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **markdown widget:** adds keyboard shortcuts ([#3005](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3005)) ([#3582](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3582)) ([99071c1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/99071c14e4a03d9897b21f1a43a5104510521dda))
|
||||
|
||||
# [2.11.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.10.2...decap-cms-widget-markdown@2.11.0) (2020-04-07)
|
||||
|
||||
### Features
|
||||
|
||||
- **yaml:** support comments ([#3529](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3529)) ([4afbbdd](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/4afbbdd8a99241d239f28c5be544bb0ca77e345b))
|
||||
|
||||
## [2.10.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.10.1...decap-cms-widget-markdown@2.10.2) (2020-03-30)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [2.10.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.10.0...decap-cms-widget-markdown@2.10.1) (2020-03-19)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
# [2.10.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.9.3...decap-cms-widget-markdown@2.10.0) (2020-03-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- ja locale labels ([#3367](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3367)) ([50837b0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/50837b0068ac8972ce16cbf5f238aa5a2c5bd6e9))
|
||||
|
||||
### Features
|
||||
|
||||
- Configure included editor components per field, add optional minimal height ([#3299](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3299)) ([b7b4bcb](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/b7b4bcb609fd90554c82bfd685f4af1b818083c1))
|
||||
|
||||
## [2.9.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.9.2...decap-cms-widget-markdown@2.9.3) (2020-02-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **widget-markdown:** don't add duplicate marks ([#3290](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3290)) ([2a0aef2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/2a0aef27d1c1c7eac146f890d896233262322c7f))
|
||||
|
||||
## [2.9.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.9.1...decap-cms-widget-markdown@2.9.2) (2020-02-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **widget-markdown:** allow shortcodes as list items ([#3278](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3278)) ([cdd3747](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/cdd3747850cca77f61b663cbfeda9765a72eb8d0))
|
||||
|
||||
## [2.9.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.9.0...decap-cms-widget-markdown@2.9.1) (2020-02-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- change getAsset to not return a promise ([#3232](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3232)) ([ab685e8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/ab685e85943d1ac48142f157683bc2126fd6af16))
|
||||
|
||||
# [2.9.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.8.4...decap-cms-widget-markdown@2.9.0) (2020-02-10)
|
||||
|
||||
### Features
|
||||
|
||||
- field based media/public folders ([#3208](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3208)) ([97bc0c8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/97bc0c8dc489e736f89d748ba832d78400fe4332))
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "chore(release): publish" ([a015d1d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/a015d1d92a4b1c0130c44fcef1c9ecdb157a0f07))
|
||||
|
||||
## [2.8.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.8.3...decap-cms-widget-markdown@2.8.4) (2020-02-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **locale:** remove hard coded strings ([#3193](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3193)) ([fc91bf8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/fc91bf8781e65ce1dc946363dbb10419a145c66b))
|
||||
|
||||
## [2.8.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.8.2...decap-cms-widget-markdown@2.8.3) (2020-02-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **editor:** merge adjacent text nodes with same marks ([#3173](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3173)) ([b4c5fc7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/b4c5fc77839a7459fd2733f105514a86c8c43c22))
|
||||
|
||||
## [2.8.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.8.1...decap-cms-widget-markdown@2.8.2) (2020-01-15)
|
||||
|
||||
### Reverts
|
||||
|
||||
- don't force multiline flag for editor component patterns ([#3089](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3089)) ([c4cbae7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/c4cbae77255d1f422fd62258a01007956d512392))
|
||||
|
||||
## [2.8.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.8.0...decap-cms-widget-markdown@2.8.1) (2020-01-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **core:** force multiline flag for editor component patterns ([#3082](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3082)) ([476f450](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/476f45096efa1723936a73f2e2e04d5c7ccd293f))
|
||||
- **widget-markdown:** allow multiline shortcodes ([#3066](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3066)) ([2929909](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/29299097cff0c280d2e97d37fe3b9888a3067554))
|
||||
- **widget-markdown:** ensure remarkToSlate result matches slate schema ([#3085](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3085)) ([fde0c5a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/fde0c5a9a776dc814bbe3a483aa286f46f45d98d))
|
||||
- **widget-markdown:** stop double pasting in raw editor ([#3083](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3083)) ([09564bf](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/09564bf8b64b2e431faf2421576b4010f05d516d))
|
||||
|
||||
# [2.8.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.8.0-beta.0...decap-cms-widget-markdown@2.8.0) (2020-01-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **widget-markdown:** cut/copy selection only in raw mode ([#3024](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/3024)) ([1b755b3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/1b755b3be8e383fc8878a1485bd0ded2fc04025c))
|
||||
- avoid nested select widget z-index conflicts ([#2990](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/2990)) ([fe09720](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/fe097202f0220b2eab426848b928258524ba6e72))
|
||||
|
||||
# [2.8.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.7.0...decap-cms-widget-markdown@2.8.0-beta.0) (2019-12-18)
|
||||
|
||||
### Features
|
||||
|
||||
- bundle assets with content ([#2958](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/2958)) ([2b41d8a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/2b41d8a838a9c8a6b21cde2ddd16b9288334e298))
|
||||
|
||||
# [2.7.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.7.0-beta.0...decap-cms-widget-markdown@2.7.0) (2019-12-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
# [2.7.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.6.1-beta.0...decap-cms-widget-markdown@2.7.0-beta.0) (2019-12-16)
|
||||
|
||||
### Features
|
||||
|
||||
- Code Widget + Markdown Widget Internal Overhaul ([#2828](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/2828)) ([18c579d](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/18c579d0e9f0ff71ed8c52f5c66f2309259af054))
|
||||
|
||||
## [2.6.1-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.6.0...decap-cms-widget-markdown@2.6.1-beta.0) (2019-12-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **widget-markdown:** fix carriage return issue ([#2899](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/2899)) ([1ff9db0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/1ff9db0915e93dab3f1c96459abf929d40398f85))
|
||||
|
||||
# [2.6.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.5.2...decap-cms-widget-markdown@2.6.0) (2019-11-18)
|
||||
|
||||
### Features
|
||||
|
||||
- **widget-markdown:** add headings dropdown ([#2879](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/2879)) ([78face3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/78face334f2dc7c99f5805551c052587e54d5753))
|
||||
|
||||
## [2.5.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.5.1...decap-cms-widget-markdown@2.5.2) (2019-11-18)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [2.5.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.5.0...decap-cms-widget-markdown@2.5.1) (2019-07-24)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
# [2.5.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.5.0-beta.1...decap-cms-widget-markdown@2.5.0) (2019-06-14)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
# [2.5.0-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.5.0-beta.0...decap-cms-widget-markdown@2.5.0-beta.1) (2019-05-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **widget-markdown:** ensure correct value on list reorder ([#2298](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/2298)) ([60caca0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/60caca0))
|
||||
|
||||
# [2.5.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.4.2...decap-cms-widget-markdown@2.5.0-beta.0) (2019-04-10)
|
||||
|
||||
### Features
|
||||
|
||||
- **editor-components:** match any characters with shortcodes ([#2268](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/2268)) ([14b6292](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/14b6292))
|
||||
|
||||
## [2.4.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.4.2-beta.0...decap-cms-widget-markdown@2.4.2) (2019-04-10)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [2.4.2-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.4.1...decap-cms-widget-markdown@2.4.2-beta.0) (2019-04-05)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [2.4.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.4.1-beta.2...decap-cms-widget-markdown@2.4.1) (2019-03-29)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [2.4.1-beta.2](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.4.1-beta.1...decap-cms-widget-markdown@2.4.1-beta.2) (2019-03-28)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
## [2.4.1-beta.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.4.1-beta.0...decap-cms-widget-markdown@2.4.1-beta.1) (2019-03-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- export on decap-cms and maps on esm ([#2244](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/2244)) ([6ffd13b](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/6ffd13b))
|
||||
|
||||
## [2.4.1-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.4.0...decap-cms-widget-markdown@2.4.1-beta.0) (2019-03-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update peer dep versions ([#2234](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/2234)) ([7987091](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/7987091))
|
||||
|
||||
# [2.4.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.3.0...decap-cms-widget-markdown@2.4.0) (2019-03-22)
|
||||
|
||||
### Features
|
||||
|
||||
- add ES module builds ([#2215](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/2215)) ([d142b32](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/d142b32))
|
||||
|
||||
# [2.3.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.3.0-beta.0...decap-cms-widget-markdown@2.3.0) (2019-03-22)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
# [2.3.0-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.2.1-beta.0...decap-cms-widget-markdown@2.3.0-beta.0) (2019-03-21)
|
||||
|
||||
### Features
|
||||
|
||||
- provide usable UMD builds for all packages ([#2141](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/2141)) ([82cc794](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/82cc794))
|
||||
|
||||
## [2.2.1-beta.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.2.0...decap-cms-widget-markdown@2.2.1-beta.0) (2019-03-15)
|
||||
|
||||
### Features
|
||||
|
||||
- upgrade to Emotion 10 ([#2166](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/2166)) ([ccef446](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/ccef446))
|
||||
|
||||
# [2.2.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.1.1...decap-cms-widget-markdown@2.2.0) (2019-03-08)
|
||||
|
||||
### Features
|
||||
|
||||
- **core:** recover entry after unexpected quit ([#2129](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/2129)) ([686504a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/686504a))
|
||||
|
||||
## [2.1.1](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.1.0...decap-cms-widget-markdown@2.1.1) (2019-02-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **markdown-widget:** handle leading or trailing whitespace ([#1517](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/1517)) ([ade03d0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/ade03d0))
|
||||
|
||||
# [2.1.0](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.0.10...decap-cms-widget-markdown@2.1.0) (2018-12-11)
|
||||
|
||||
### Features
|
||||
|
||||
- **editor-components:** support title in image component ([#1862](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/1862)) ([cbb7762](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/cbb7762))
|
||||
|
||||
## [2.0.10](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.0.9...decap-cms-widget-markdown@2.0.10) (2018-11-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **decap-cms-widget-markdown:** add missing border radius on toolbar ([#1905](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/1905)) ([3772171](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/3772171))
|
||||
|
||||
## [2.0.9](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.0.8...decap-cms-widget-markdown@2.0.9) (2018-11-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **editor-component-image:** fix null on empty markdown image alt ([#1778](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/1778)) ([9b72419](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/9b72419))
|
||||
- **editor-components:** fix default value processing ([#1848](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/1848)) ([a0cfa1a](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/a0cfa1a))
|
||||
|
||||
<a name="2.0.8"></a>
|
||||
|
||||
## [2.0.8](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.0.7...decap-cms-widget-markdown@2.0.8) (2018-09-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add support for default field values in editor components ([#1616](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/issues/1616)) ([0d01809](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/commit/0d01809))
|
||||
|
||||
<a name="2.0.7"></a>
|
||||
|
||||
## [2.0.7](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.0.6...decap-cms-widget-markdown@2.0.7) (2018-08-27)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
<a name="2.0.6"></a>
|
||||
|
||||
## [2.0.6](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.0.5...decap-cms-widget-markdown@2.0.6) (2018-08-24)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
<a name="2.0.5"></a>
|
||||
|
||||
## [2.0.5](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.0.4...decap-cms-widget-markdown@2.0.5) (2018-08-07)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
<a name="2.0.4"></a>
|
||||
|
||||
## [2.0.4](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.0.3...decap-cms-widget-markdown@2.0.4) (2018-08-01)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
<a name="2.0.3"></a>
|
||||
|
||||
## [2.0.3](https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown/compare/decap-cms-widget-markdown@2.0.2...decap-cms-widget-markdown@2.0.3) (2018-07-28)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
|
||||
<a name="2.0.2"></a>
|
||||
|
||||
## 2.0.2 (2018-07-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- bug fixes from linters ([#1524](https://github.com/decaporg/decap-cms/issues/1524)) ([6632e5d](https://github.com/decaporg/decap-cms/commit/6632e5d))
|
||||
|
||||
<a name="2.0.1"></a>
|
||||
|
||||
## 2.0.1 (2018-07-26)
|
||||
|
||||
<a name="2.0.0"></a>
|
||||
|
||||
# 2.0.0 (2018-07-26)
|
||||
|
||||
**Note:** Version bump only for package decap-cms-widget-markdown
|
||||
@@ -0,0 +1,9 @@
|
||||
# Docs coming soon!
|
||||
|
||||
Decap CMS was converted from a single npm package to a "monorepo" of over 20 packages.
|
||||
We haven't created a README for this package yet, but you can:
|
||||
|
||||
1. Check out the [main readme](https://github.com/decaporg/decap-cms/#readme) or the [documentation
|
||||
site](https://www.decapcms.org) for more info.
|
||||
2. Reach out to the [community chat](https://decapcms.org/chat/) if you need help.
|
||||
3. Help out and [write the readme yourself](https://github.com/decaporg/decap-cms/edit/main/packages/decap-cms-widget-markdown/README.md)!
|
||||
65
source/admin/packages/decap-cms-widget-markdown/package.json
Normal file
65
source/admin/packages/decap-cms-widget-markdown/package.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "decap-cms-widget-markdown",
|
||||
"description": "Widget for editing markdown in Decap CMS.",
|
||||
"version": "3.5.0",
|
||||
"homepage": "https://www.decapcms.org/docs/widgets/#markdown",
|
||||
"repository": "https://github.com/decaporg/decap-cms/tree/main/packages/decap-cms-widget-markdown",
|
||||
"bugs": "https://github.com/decaporg/decap-cms/issues",
|
||||
"module": "dist/esm/index.js",
|
||||
"main": "dist/decap-cms-widget-markdown.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"decap-cms",
|
||||
"widget",
|
||||
"markdown",
|
||||
"editor"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"develop": "npm run build:esm -- --watch",
|
||||
"build": "cross-env NODE_ENV=production webpack",
|
||||
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward"
|
||||
},
|
||||
"dependencies": {
|
||||
"detab": "^2.0.4",
|
||||
"dompurify": "^3.2.6",
|
||||
"is-hotkey": "^0.2.0",
|
||||
"is-url": "^1.2.4",
|
||||
"mdast-util-definitions": "^1.2.3",
|
||||
"mdast-util-to-string": "^1.0.5",
|
||||
"rehype-parse": "^6.0.0",
|
||||
"rehype-remark": "^8.0.0",
|
||||
"rehype-stringify": "^7.0.0",
|
||||
"remark-parse": "^6.0.3",
|
||||
"remark-rehype": "^4.0.0",
|
||||
"remark-slate": "^1.8.6",
|
||||
"remark-slate-transformer": "^0.7.4",
|
||||
"remark-stringify": "^6.0.4",
|
||||
"slate": "^0.91.1",
|
||||
"slate-base64-serializer": "^0.2.107",
|
||||
"slate-history": "^0.93.0",
|
||||
"slate-hyperscript": "^0.77.0",
|
||||
"slate-plain-serializer": "^0.7.1",
|
||||
"slate-react": "^0.91.2",
|
||||
"slate-soft-break": "^0.9.0",
|
||||
"unified": "^9.2.0",
|
||||
"unist-builder": "^1.0.3",
|
||||
"unist-util-visit-parents": "^2.0.1",
|
||||
"vfile-location": "^2.0.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"decap-cms-ui-default": "^3.0.0",
|
||||
"immutable": "^3.7.6",
|
||||
"lodash": "^4.17.11",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-immutable-proptypes": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"commonmark": "^0.30.0",
|
||||
"commonmark-spec": "^0.30.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { ClassNames } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { lengths, fonts } from 'decap-cms-ui-default';
|
||||
import { createEditor } from 'slate';
|
||||
import { Editable, ReactEditor, Slate, withReact } from 'slate-react';
|
||||
import { withHistory } from 'slate-history';
|
||||
|
||||
import { editorStyleVars, EditorControlBar } from '../styles';
|
||||
import Toolbar from './Toolbar';
|
||||
import defaultEmptyBlock from './plugins/blocks/defaultEmptyBlock';
|
||||
|
||||
function rawEditorStyles({ minimal }) {
|
||||
return `
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
min-height: ${minimal ? 'auto' : lengths.richTextEditorMinHeight};
|
||||
font-family: ${fonts.mono};
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-top: 0;
|
||||
margin-top: -${editorStyleVars.stickyDistanceBottom};
|
||||
`;
|
||||
}
|
||||
|
||||
const RawEditorContainer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
function RawEditor(props) {
|
||||
const { className, field, isShowModeToggle, t, onChange } = props;
|
||||
|
||||
const editor = useMemo(() => withReact(withHistory(createEditor())), []);
|
||||
|
||||
const [value, setValue] = useState(
|
||||
props.value
|
||||
? props.value.split('\n').map(line => defaultEmptyBlock(line))
|
||||
: [defaultEmptyBlock()],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.pendingFocus) {
|
||||
ReactEditor.focus(editor);
|
||||
props.pendingFocus();
|
||||
}
|
||||
}, [props.pendingFocus]);
|
||||
|
||||
function handleToggleMode() {
|
||||
props.onMode('rich_text');
|
||||
}
|
||||
|
||||
function handleChange(value) {
|
||||
onChange(value.map(line => line.children[0].text).join('\n'));
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<Slate editor={editor} value={value} onChange={handleChange}>
|
||||
<RawEditorContainer>
|
||||
<EditorControlBar>
|
||||
<Toolbar
|
||||
onToggleMode={handleToggleMode}
|
||||
buttons={field.get('buttons')}
|
||||
disabled
|
||||
rawMode
|
||||
isShowModeToggle={isShowModeToggle}
|
||||
t={t}
|
||||
/>
|
||||
</EditorControlBar>
|
||||
<ClassNames>
|
||||
{({ css, cx }) => (
|
||||
<Editable
|
||||
className={cx(
|
||||
className,
|
||||
css`
|
||||
${rawEditorStyles({ minimal: field.get('minimal') })}
|
||||
`,
|
||||
)}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)}
|
||||
</ClassNames>
|
||||
</RawEditorContainer>
|
||||
</Slate>
|
||||
);
|
||||
}
|
||||
|
||||
RawEditor.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onMode: PropTypes.func.isRequired,
|
||||
className: PropTypes.string.isRequired,
|
||||
value: PropTypes.string,
|
||||
field: ImmutablePropTypes.map.isRequired,
|
||||
isShowModeToggle: PropTypes.bool.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default RawEditor;
|
||||
@@ -0,0 +1,283 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled from '@emotion/styled';
|
||||
import { css } from '@emotion/react';
|
||||
import { List } from 'immutable';
|
||||
import {
|
||||
Toggle,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownButton,
|
||||
colors,
|
||||
transitions,
|
||||
lengths,
|
||||
} from 'decap-cms-ui-default';
|
||||
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
|
||||
const ToolbarContainer = styled.div`
|
||||
background-color: ${colors.textFieldBorder};
|
||||
border-top-right-radius: ${lengths.borderRadius};
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 11px 14px;
|
||||
min-height: 58px;
|
||||
transition: background-color ${transitions.main}, color ${transitions.main};
|
||||
color: ${colors.text};
|
||||
`;
|
||||
|
||||
const ToolbarDropdownWrapper = styled.div`
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const ToolbarToggle = styled.div`
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
margin: 0 10px;
|
||||
`;
|
||||
|
||||
const StyledToggle = ToolbarToggle.withComponent(Toggle);
|
||||
|
||||
const ToolbarToggleLabel = styled.span`
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
line-height: 20px;
|
||||
min-width: ${props => (props.offPosition ? '62px' : '70px')};
|
||||
|
||||
${props =>
|
||||
props.isActive &&
|
||||
css`
|
||||
font-weight: 600;
|
||||
color: ${colors.active};
|
||||
`};
|
||||
`;
|
||||
|
||||
export default class Toolbar extends React.Component {
|
||||
static propTypes = {
|
||||
buttons: ImmutablePropTypes.list,
|
||||
editorComponents: ImmutablePropTypes.list,
|
||||
onToggleMode: PropTypes.func.isRequired,
|
||||
rawMode: PropTypes.bool,
|
||||
isShowModeToggle: PropTypes.bool.isRequired,
|
||||
plugins: ImmutablePropTypes.map,
|
||||
onSubmit: PropTypes.func,
|
||||
onAddAsset: PropTypes.func,
|
||||
getAsset: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
onMarkClick: PropTypes.func,
|
||||
onBlockClick: PropTypes.func,
|
||||
onLinkClick: PropTypes.func,
|
||||
hasMark: PropTypes.func,
|
||||
hasInline: PropTypes.func,
|
||||
hasBlock: PropTypes.func,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// Manually validate PropTypes - React 19 breaking change
|
||||
PropTypes.checkPropTypes(Toolbar.propTypes, this.props, 'prop', 'Toolbar');
|
||||
}
|
||||
|
||||
isVisible = button => {
|
||||
const { buttons } = this.props;
|
||||
return !List.isList(buttons) || buttons.includes(button);
|
||||
};
|
||||
|
||||
handleBlockClick = (event, type) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
this.props.onBlockClick(type);
|
||||
};
|
||||
|
||||
handleMarkClick = (event, type) => {
|
||||
event.preventDefault();
|
||||
this.props.onMarkClick(type);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
onLinkClick,
|
||||
onToggleMode,
|
||||
rawMode,
|
||||
isShowModeToggle,
|
||||
plugins,
|
||||
disabled,
|
||||
onSubmit,
|
||||
hasMark = () => {},
|
||||
hasInline = () => {},
|
||||
hasBlock = () => {},
|
||||
hasQuote = () => {},
|
||||
hasListItems = () => {},
|
||||
editorComponents,
|
||||
t,
|
||||
} = this.props;
|
||||
const isVisible = this.isVisible;
|
||||
const showEditorComponents = !editorComponents || editorComponents.size >= 1;
|
||||
|
||||
function showPlugin({ id }) {
|
||||
return editorComponents ? editorComponents.includes(id) : true;
|
||||
}
|
||||
|
||||
const pluginsList = plugins ? plugins.toList().filter(showPlugin) : List();
|
||||
|
||||
const headingOptions = {
|
||||
'heading-one': t('editor.editorWidgets.headingOptions.headingOne'),
|
||||
'heading-two': t('editor.editorWidgets.headingOptions.headingTwo'),
|
||||
'heading-three': t('editor.editorWidgets.headingOptions.headingThree'),
|
||||
'heading-four': t('editor.editorWidgets.headingOptions.headingFour'),
|
||||
'heading-five': t('editor.editorWidgets.headingOptions.headingFive'),
|
||||
'heading-six': t('editor.editorWidgets.headingOptions.headingSix'),
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolbarContainer>
|
||||
<div>
|
||||
{isVisible('bold') && (
|
||||
<ToolbarButton
|
||||
type="bold"
|
||||
label={t('editor.editorWidgets.markdown.bold')}
|
||||
icon="bold"
|
||||
onClick={this.handleMarkClick}
|
||||
isActive={hasMark('bold')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
{isVisible('italic') && (
|
||||
<ToolbarButton
|
||||
type="italic"
|
||||
label={t('editor.editorWidgets.markdown.italic')}
|
||||
icon="italic"
|
||||
onClick={this.handleMarkClick}
|
||||
isActive={hasMark('italic')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
{isVisible('code') && (
|
||||
<ToolbarButton
|
||||
type="code"
|
||||
label={t('editor.editorWidgets.markdown.code')}
|
||||
icon="code"
|
||||
onClick={this.handleMarkClick}
|
||||
isActive={hasMark('code')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
{isVisible('link') && (
|
||||
<ToolbarButton
|
||||
type="link"
|
||||
label={t('editor.editorWidgets.markdown.link')}
|
||||
icon="link"
|
||||
onClick={onLinkClick}
|
||||
isActive={hasInline('link')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
{/* Show dropdown if at least one heading is not hidden */}
|
||||
{Object.keys(headingOptions).some(isVisible) && (
|
||||
<ToolbarDropdownWrapper>
|
||||
<Dropdown
|
||||
dropdownWidth="max-content"
|
||||
dropdownTopOverlap="36px"
|
||||
renderButton={() => (
|
||||
<DropdownButton>
|
||||
<ToolbarButton
|
||||
type="headings"
|
||||
label={t('editor.editorWidgets.markdown.headings')}
|
||||
icon="hOptions"
|
||||
disabled={disabled}
|
||||
isActive={!disabled && Object.keys(headingOptions).some(hasBlock)}
|
||||
/>
|
||||
</DropdownButton>
|
||||
)}
|
||||
>
|
||||
{!disabled &&
|
||||
Object.keys(headingOptions).map(
|
||||
(optionKey, idx) =>
|
||||
isVisible(optionKey) && (
|
||||
<DropdownItem
|
||||
key={idx}
|
||||
label={headingOptions[optionKey]}
|
||||
className={hasBlock(optionKey) ? 'active' : ''}
|
||||
onClick={() => this.handleBlockClick(null, optionKey)}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</Dropdown>
|
||||
</ToolbarDropdownWrapper>
|
||||
)}
|
||||
{isVisible('quote') && (
|
||||
<ToolbarButton
|
||||
type="quote"
|
||||
label={t('editor.editorWidgets.markdown.quote')}
|
||||
icon="quote"
|
||||
onClick={this.handleBlockClick}
|
||||
isActive={hasQuote('quote')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
{isVisible('bulleted-list') && (
|
||||
<ToolbarButton
|
||||
type="bulleted-list"
|
||||
label={t('editor.editorWidgets.markdown.bulletedList')}
|
||||
icon="list-bulleted"
|
||||
onClick={this.handleBlockClick}
|
||||
isActive={hasListItems('bulleted-list')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
{isVisible('numbered-list') && (
|
||||
<ToolbarButton
|
||||
type="numbered-list"
|
||||
label={t('editor.editorWidgets.markdown.numberedList')}
|
||||
icon="list-numbered"
|
||||
onClick={this.handleBlockClick}
|
||||
isActive={hasListItems('numbered-list')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
{showEditorComponents && (
|
||||
<ToolbarDropdownWrapper>
|
||||
<Dropdown
|
||||
dropdownTopOverlap="36px"
|
||||
dropdownWidth="max-content"
|
||||
renderButton={() => (
|
||||
<DropdownButton>
|
||||
<ToolbarButton
|
||||
label={t('editor.editorWidgets.markdown.addComponent')}
|
||||
icon="add-with"
|
||||
onClick={this.handleComponentsMenuToggle}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</DropdownButton>
|
||||
)}
|
||||
>
|
||||
{pluginsList.map((plugin, idx) => (
|
||||
<DropdownItem key={idx} label={plugin.label} onClick={() => onSubmit(plugin)} />
|
||||
))}
|
||||
</Dropdown>
|
||||
</ToolbarDropdownWrapper>
|
||||
)}
|
||||
</div>
|
||||
{isShowModeToggle && (
|
||||
<ToolbarToggle>
|
||||
<ToolbarToggleLabel isActive={!rawMode} offPosition>
|
||||
{t('editor.editorWidgets.markdown.richText')}
|
||||
</ToolbarToggleLabel>
|
||||
<StyledToggle active={rawMode} onChange={onToggleMode} />
|
||||
<ToolbarToggleLabel isActive={rawMode}>
|
||||
{t('editor.editorWidgets.markdown.markdown')}
|
||||
</ToolbarToggleLabel>
|
||||
</ToolbarToggle>
|
||||
)}
|
||||
</ToolbarContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from '@emotion/styled';
|
||||
import { Icon, buttons } from 'decap-cms-ui-default';
|
||||
|
||||
const StyledToolbarButton = styled.button`
|
||||
${buttons.button};
|
||||
display: inline-block;
|
||||
padding: 6px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
font-size: 16px;
|
||||
color: ${props => (props.isActive ? '#1e2532' : 'inherit')};
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
cursor: auto;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
${Icon} {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
function ToolbarButton({ type, label, icon, onClick, isActive, disabled }) {
|
||||
return (
|
||||
<StyledToolbarButton
|
||||
isActive={isActive}
|
||||
onClick={e => onClick && onClick(e, type)}
|
||||
title={label}
|
||||
disabled={disabled}
|
||||
>
|
||||
{icon ? <Icon type={icon} /> : label}
|
||||
</StyledToolbarButton>
|
||||
);
|
||||
}
|
||||
|
||||
ToolbarButton.propTypes = {
|
||||
type: PropTypes.string,
|
||||
label: PropTypes.string.isRequired,
|
||||
icon: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
isActive: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ToolbarButton;
|
||||
@@ -0,0 +1,297 @@
|
||||
// @refresh reset
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { ClassNames, css as coreCss } from '@emotion/react';
|
||||
import { lengths, fonts, zIndex } from 'decap-cms-ui-default';
|
||||
import styled from '@emotion/styled';
|
||||
import { createEditor, Transforms, Editor as SlateEditor } from 'slate';
|
||||
import { Editable, ReactEditor, Slate, withReact } from 'slate-react';
|
||||
import { withHistory } from 'slate-history';
|
||||
import { fromJS } from 'immutable';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
import { editorStyleVars, EditorControlBar } from '../styles';
|
||||
import Toolbar from './Toolbar';
|
||||
import { Element, Leaf } from './renderers';
|
||||
import withLists from './plugins/lists/withLists';
|
||||
import withBlocks from './plugins/blocks/withBlocks';
|
||||
import withInlines from './plugins/inlines/withInlines';
|
||||
import toggleMark from './plugins/inlines/events/toggleMark';
|
||||
import toggleLink from './plugins/inlines/events/toggleLink';
|
||||
import getActiveLink from './plugins/inlines/selectors/getActiveLink';
|
||||
import isMarkActive from './plugins/inlines/locations/isMarkActive';
|
||||
import isCursorInBlockType from './plugins/blocks/locations/isCursorInBlockType';
|
||||
import { markdownToSlate, slateToMarkdown } from '../serializers';
|
||||
import withShortcodes from './plugins/shortcodes/withShortcodes';
|
||||
import insertShortcode from './plugins/shortcodes/insertShortcode';
|
||||
import defaultEmptyBlock from './plugins/blocks/defaultEmptyBlock';
|
||||
import withHtml from './plugins/html/withHtml';
|
||||
|
||||
function visualEditorStyles({ minimal }) {
|
||||
return `
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
font-family: ${fonts.primary};
|
||||
min-height: ${minimal ? 'auto' : lengths.richTextEditorMinHeight};
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-top: 0;
|
||||
margin-top: -${editorStyleVars.stickyDistanceBottom};
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: ${zIndex.zIndex100};
|
||||
`;
|
||||
}
|
||||
|
||||
const InsertionPoint = styled.div`
|
||||
flex: 1 1 auto;
|
||||
cursor: text;
|
||||
`;
|
||||
|
||||
export function mergeMediaConfig(editorComponents, field) {
|
||||
// merge editor media library config to image components
|
||||
if (editorComponents.has('image')) {
|
||||
const imageComponent = editorComponents.get('image');
|
||||
const fields = imageComponent?.fields;
|
||||
|
||||
if (fields) {
|
||||
imageComponent.fields = fields.update(
|
||||
fields.findIndex(f => f.get('widget') === 'image'),
|
||||
f => {
|
||||
// merge `media_library` config
|
||||
if (field.has('media_library')) {
|
||||
f = f.set(
|
||||
'media_library',
|
||||
field.get('media_library').mergeDeep(f.get('media_library')),
|
||||
);
|
||||
}
|
||||
// merge 'media_folder'
|
||||
if (field.has('media_folder') && !f.has('media_folder')) {
|
||||
f = f.set('media_folder', field.get('media_folder'));
|
||||
}
|
||||
// merge 'public_folder'
|
||||
if (field.has('public_folder') && !f.has('public_folder')) {
|
||||
f = f.set('public_folder', field.get('public_folder'));
|
||||
}
|
||||
return f;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Editor(props) {
|
||||
const {
|
||||
onAddAsset,
|
||||
getAsset,
|
||||
className,
|
||||
field,
|
||||
isShowModeToggle,
|
||||
t,
|
||||
isDisabled,
|
||||
getEditorComponents,
|
||||
getRemarkPlugins,
|
||||
onChange,
|
||||
} = props;
|
||||
|
||||
const editor = useMemo(
|
||||
() =>
|
||||
withHtml(
|
||||
withReact(withHistory(withShortcodes(withBlocks(withLists(withInlines(createEditor())))))),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const emptyValue = [defaultEmptyBlock()];
|
||||
let editorComponents = getEditorComponents();
|
||||
const codeBlockComponent = fromJS(editorComponents.find(({ type }) => type === 'code-block'));
|
||||
|
||||
editorComponents =
|
||||
codeBlockComponent || editorComponents.has('code-block')
|
||||
? editorComponents
|
||||
: editorComponents.set('code-block', { label: 'Code Block', type: 'code-block' });
|
||||
|
||||
mergeMediaConfig(editorComponents, field);
|
||||
|
||||
const [editorValue, setEditorValue] = useState(
|
||||
props.value
|
||||
? markdownToSlate(props.value, {
|
||||
voidCodeBlock: !!codeBlockComponent,
|
||||
remarkPlugins: getRemarkPlugins(),
|
||||
})
|
||||
: emptyValue,
|
||||
);
|
||||
|
||||
const renderElement = useCallback(
|
||||
props => (
|
||||
<Element {...props} classNameWrapper={className} codeBlockComponent={codeBlockComponent} />
|
||||
),
|
||||
[],
|
||||
);
|
||||
const renderLeaf = useCallback(props => <Leaf {...props} />, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.pendingFocus) {
|
||||
ReactEditor.focus(editor);
|
||||
props.pendingFocus();
|
||||
}
|
||||
}, [props.pendingFocus]);
|
||||
|
||||
function handleMarkClick(format) {
|
||||
ReactEditor.focus(editor);
|
||||
toggleMark(editor, format);
|
||||
}
|
||||
|
||||
function handleBlockClick(format) {
|
||||
ReactEditor.focus(editor);
|
||||
if (format.endsWith('-list')) {
|
||||
editor.toggleList(format);
|
||||
} else {
|
||||
editor.toggleBlock(format);
|
||||
}
|
||||
}
|
||||
|
||||
function handleLinkClick() {
|
||||
toggleLink(editor, t('editor.editorWidgets.markdown.linkPrompt'));
|
||||
ReactEditor.focus(editor);
|
||||
}
|
||||
|
||||
function handleToggleMode() {
|
||||
props.onMode('raw');
|
||||
}
|
||||
|
||||
function handleInsertShortcode(pluginConfig) {
|
||||
insertShortcode(editor, pluginConfig);
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
for (const handler of editor.keyDownHandlers || []) {
|
||||
if (handler(event, editor) === false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ReactEditor.focus(editor);
|
||||
}
|
||||
|
||||
function handleClickBelowDocument() {
|
||||
ReactEditor.focus(editor);
|
||||
Transforms.select(editor, { path: [0, 0], offset: 0 });
|
||||
Transforms.select(editor, SlateEditor.end(editor, []));
|
||||
}
|
||||
const [toolbarKey, setToolbarKey] = useState(0);
|
||||
|
||||
function handleChange(newValue) {
|
||||
if (!isEqual(newValue, editorValue)) {
|
||||
setEditorValue(() => newValue);
|
||||
onChange(
|
||||
slateToMarkdown(newValue, {
|
||||
voidCodeBlock: !!codeBlockComponent,
|
||||
remarkPlugins: getRemarkPlugins(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
setToolbarKey(prev => prev + 1);
|
||||
}
|
||||
|
||||
function hasMark(format) {
|
||||
return isMarkActive(editor, format);
|
||||
}
|
||||
|
||||
function hasInline(format) {
|
||||
if (format == 'link') {
|
||||
return !!getActiveLink(editor);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasBlock(format) {
|
||||
return isCursorInBlockType(editor, format);
|
||||
}
|
||||
function hasQuote() {
|
||||
return isCursorInBlockType(editor, 'quote');
|
||||
}
|
||||
function hasListItems(type) {
|
||||
return isCursorInBlockType(editor, type);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
css={coreCss`
|
||||
position: relative;
|
||||
`}
|
||||
>
|
||||
<Slate editor={editor} value={editorValue} onChange={handleChange}>
|
||||
<EditorControlBar>
|
||||
{
|
||||
<Toolbar
|
||||
key={toolbarKey}
|
||||
onMarkClick={handleMarkClick}
|
||||
onBlockClick={handleBlockClick}
|
||||
onLinkClick={handleLinkClick}
|
||||
onToggleMode={handleToggleMode}
|
||||
plugins={editorComponents}
|
||||
onSubmit={handleInsertShortcode}
|
||||
onAddAsset={onAddAsset}
|
||||
getAsset={getAsset}
|
||||
buttons={field.get('buttons')}
|
||||
editorComponents={field.get('editor_components')}
|
||||
hasMark={hasMark}
|
||||
hasInline={hasInline}
|
||||
hasBlock={hasBlock}
|
||||
hasQuote={hasQuote}
|
||||
hasListItems={hasListItems}
|
||||
isShowModeToggle={isShowModeToggle}
|
||||
t={t}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
}
|
||||
</EditorControlBar>
|
||||
{
|
||||
<ClassNames>
|
||||
{({ css, cx }) => (
|
||||
<div
|
||||
className={cx(
|
||||
className,
|
||||
css`
|
||||
${visualEditorStyles({ minimal: field.get('minimal') })}
|
||||
`,
|
||||
)}
|
||||
>
|
||||
{editorValue.length !== 0 && (
|
||||
<Editable
|
||||
className={css`
|
||||
padding: 16px 20px 0;
|
||||
`}
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus={false}
|
||||
/>
|
||||
)}
|
||||
<InsertionPoint onClick={handleClickBelowDocument} />
|
||||
</div>
|
||||
)}
|
||||
</ClassNames>
|
||||
}
|
||||
</Slate>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Editor.propTypes = {
|
||||
onAddAsset: PropTypes.func.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onMode: PropTypes.func.isRequired,
|
||||
className: PropTypes.string.isRequired,
|
||||
value: PropTypes.string,
|
||||
field: ImmutablePropTypes.map.isRequired,
|
||||
getEditorComponents: PropTypes.func.isRequired,
|
||||
getRemarkPlugins: PropTypes.func.isRequired,
|
||||
isShowModeToggle: PropTypes.bool.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Map, fromJS } from 'immutable';
|
||||
|
||||
import { mergeMediaConfig } from '../VisualEditor';
|
||||
|
||||
describe('VisualEditor', () => {
|
||||
describe('mergeMediaConfig', () => {
|
||||
it('should copy editor media settings to image component', () => {
|
||||
const editorComponents = Map({
|
||||
image: {
|
||||
id: 'image',
|
||||
label: 'Image',
|
||||
type: 'shortcode',
|
||||
icon: 'exclamation-triangle',
|
||||
widget: 'object',
|
||||
pattern: {},
|
||||
fields: fromJS([
|
||||
{
|
||||
label: 'Image',
|
||||
name: 'image',
|
||||
widget: 'image',
|
||||
media_library: { allow_multiple: false },
|
||||
},
|
||||
{ label: 'Alt Text', name: 'alt' },
|
||||
{ label: 'Title', name: 'title' },
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
const field = fromJS({
|
||||
label: 'Body',
|
||||
name: 'body',
|
||||
widget: 'markdown',
|
||||
media_folder: '/{{media_folder}}/posts/images/widget/body',
|
||||
public_folder: '{{public_folder}}/posts/images/widget/body',
|
||||
media_library: { config: { max_file_size: 1234 } },
|
||||
});
|
||||
|
||||
mergeMediaConfig(editorComponents, field);
|
||||
|
||||
expect(editorComponents.get('image').fields).toEqual(
|
||||
fromJS([
|
||||
{
|
||||
label: 'Image',
|
||||
name: 'image',
|
||||
widget: 'image',
|
||||
media_library: { allow_multiple: false, config: { max_file_size: 1234 } },
|
||||
media_folder: '/{{media_folder}}/posts/images/widget/body',
|
||||
public_folder: '{{public_folder}}/posts/images/widget/body',
|
||||
},
|
||||
{ label: 'Alt Text', name: 'alt' },
|
||||
{ label: 'Title', name: 'title' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,543 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Compile markdown to Slate Raw AST should compile kitchen sink example 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "An exhibit of Markdown",
|
||||
},
|
||||
],
|
||||
"type": "heading-one",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "This note demonstrates some of what Markdown is capable of doing.",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"italic": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "italic",
|
||||
},
|
||||
],
|
||||
"text": "Note: Feel free to play with this page. Unlike regular notes, this doesn't
|
||||
automatically save itself.",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Basic formatting",
|
||||
},
|
||||
],
|
||||
"type": "heading-two",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Paragraphs can be written like so. A paragraph is the basic block of Markdown.
|
||||
A paragraph is what text will turn into when there is no reason it should
|
||||
become anything else.",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Paragraphs must be separated by a blank line. Basic formatting of ",
|
||||
},
|
||||
Object {
|
||||
"italic": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "italic",
|
||||
},
|
||||
],
|
||||
"text": "italics",
|
||||
},
|
||||
Object {
|
||||
"text": " and
|
||||
",
|
||||
},
|
||||
Object {
|
||||
"bold": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "bold",
|
||||
},
|
||||
],
|
||||
"text": "bold",
|
||||
},
|
||||
Object {
|
||||
"text": " is supported. This ",
|
||||
},
|
||||
Object {
|
||||
"italic": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "italic",
|
||||
},
|
||||
],
|
||||
"text": "can be ",
|
||||
},
|
||||
Object {
|
||||
"bold": true,
|
||||
"italic": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "italic",
|
||||
},
|
||||
Object {
|
||||
"type": "bold",
|
||||
},
|
||||
],
|
||||
"text": "nested",
|
||||
},
|
||||
Object {
|
||||
"italic": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "italic",
|
||||
},
|
||||
],
|
||||
"text": " like",
|
||||
},
|
||||
Object {
|
||||
"text": " so.",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Lists",
|
||||
},
|
||||
],
|
||||
"type": "heading-two",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Ordered list",
|
||||
},
|
||||
],
|
||||
"type": "heading-three",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Item 1 2. A second item 3. Number 3 4. Ⅳ",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "list-item",
|
||||
},
|
||||
],
|
||||
"data": Object {
|
||||
"start": 1,
|
||||
},
|
||||
"type": "numbered-list",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"italic": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "italic",
|
||||
},
|
||||
],
|
||||
"text": "Note: the fourth item uses the Unicode character for Roman numeral four.",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Unordered list",
|
||||
},
|
||||
],
|
||||
"type": "heading-three",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "An item Another item Yet another item And there's more...",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "list-item",
|
||||
},
|
||||
],
|
||||
"data": Object {
|
||||
"start": null,
|
||||
},
|
||||
"type": "bulleted-list",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Paragraph modifiers",
|
||||
},
|
||||
],
|
||||
"type": "heading-two",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Code block",
|
||||
},
|
||||
],
|
||||
"type": "heading-three",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Code blocks are very useful for developers and other people who look at
|
||||
code or other things that are written in plain text. As you can see, it
|
||||
uses a fixed-width font.",
|
||||
},
|
||||
],
|
||||
"data": Object {
|
||||
"lang": null,
|
||||
"shortcode": "code-block",
|
||||
"shortcodeData": Object {
|
||||
"code": "Code blocks are very useful for developers and other people who look at
|
||||
code or other things that are written in plain text. As you can see, it
|
||||
uses a fixed-width font.",
|
||||
"lang": null,
|
||||
},
|
||||
},
|
||||
"type": "shortcode",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "You can also make ",
|
||||
},
|
||||
Object {
|
||||
"code": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "code",
|
||||
},
|
||||
],
|
||||
"text": "inline code",
|
||||
},
|
||||
Object {
|
||||
"text": " to add code into other things.",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Quote",
|
||||
},
|
||||
],
|
||||
"type": "heading-three",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Here is a quote. What this is should be self explanatory. Quotes are
|
||||
automatically indented when they are used.",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "quote",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Headings",
|
||||
},
|
||||
],
|
||||
"type": "heading-two",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "There are six levels of headings. They correspond with the six levels of HTML
|
||||
headings. You've probably noticed them already in the page. Each level down
|
||||
uses one more hash character.",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Headings ",
|
||||
},
|
||||
Object {
|
||||
"italic": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "italic",
|
||||
},
|
||||
],
|
||||
"text": "can",
|
||||
},
|
||||
Object {
|
||||
"text": " also contain ",
|
||||
},
|
||||
Object {
|
||||
"bold": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "bold",
|
||||
},
|
||||
],
|
||||
"text": "formatting",
|
||||
},
|
||||
],
|
||||
"type": "heading-three",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "They can even contain ",
|
||||
},
|
||||
Object {
|
||||
"code": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "code",
|
||||
},
|
||||
],
|
||||
"text": "inline code",
|
||||
},
|
||||
],
|
||||
"type": "heading-three",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Of course, demonstrating what headings look like messes up the structure of the
|
||||
page.",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "I don't recommend using more than three or four levels of headings here,
|
||||
because, when you're smallest heading isn't too small, and you're largest
|
||||
heading isn't too big, and you want each size up to look noticeably larger and
|
||||
more important, there there are only so many sizes that you can use.",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "URLs",
|
||||
},
|
||||
],
|
||||
"type": "heading-two",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "URLs can be made in a handful of ways:",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "A named link to MarkItDown. The easiest way to do these is to select what you",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "list-item",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "want to make a link and hit ",
|
||||
},
|
||||
Object {
|
||||
"code": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "code",
|
||||
},
|
||||
],
|
||||
"text": "Ctrl+L",
|
||||
},
|
||||
Object {
|
||||
"text": ". Another named link to",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "list-item",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "MarkItDown",
|
||||
},
|
||||
],
|
||||
"data": Object {
|
||||
"title": null,
|
||||
"url": "http://www.markitdown.net/",
|
||||
},
|
||||
"type": "link",
|
||||
},
|
||||
Object {
|
||||
"text": " Sometimes you just want a URL like",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "list-item",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "http://www.markitdown.net/",
|
||||
},
|
||||
],
|
||||
"data": Object {
|
||||
"title": null,
|
||||
"url": "http://www.markitdown.net/",
|
||||
},
|
||||
"type": "link",
|
||||
},
|
||||
Object {
|
||||
"text": ".",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "list-item",
|
||||
},
|
||||
],
|
||||
"data": Object {
|
||||
"start": null,
|
||||
},
|
||||
"type": "bulleted-list",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Horizontal rule",
|
||||
},
|
||||
],
|
||||
"type": "heading-two",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "A horizontal rule is a line that goes across the middle of the page.",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "",
|
||||
},
|
||||
],
|
||||
"type": "thematic-break",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "It's sometimes handy for breaking things up.",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Images",
|
||||
},
|
||||
],
|
||||
"type": "heading-two",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Markdown can also contain images. I'll need to add something here sometime.",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Finally",
|
||||
},
|
||||
],
|
||||
"type": "heading-two",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "There's actually a lot more to Markdown than this. See the official
|
||||
introduction and syntax for more information. However, be aware that this is
|
||||
not using the official implementation, and this might work subtly differently
|
||||
in some of the little things.",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,668 @@
|
||||
import { markdownToSlate } from '../../serializers';
|
||||
|
||||
const parser = markdownToSlate;
|
||||
|
||||
describe('Compile markdown to Slate Raw AST', () => {
|
||||
it('should compile simple markdown', () => {
|
||||
const value = `
|
||||
# H1
|
||||
|
||||
sweet body
|
||||
`;
|
||||
expect(parser(value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "H1",
|
||||
},
|
||||
],
|
||||
"type": "heading-one",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "sweet body",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should compile a markdown ordered list', () => {
|
||||
const value = `
|
||||
# H1
|
||||
|
||||
1. yo
|
||||
2. bro
|
||||
3. fro
|
||||
`;
|
||||
expect(parser(value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "H1",
|
||||
},
|
||||
],
|
||||
"type": "heading-one",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "yo",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "list-item",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "bro",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "list-item",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "fro",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "list-item",
|
||||
},
|
||||
],
|
||||
"data": Object {
|
||||
"start": 1,
|
||||
},
|
||||
"type": "numbered-list",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should compile bulleted lists', () => {
|
||||
const value = `
|
||||
# H1
|
||||
|
||||
* yo
|
||||
* bro
|
||||
* fro
|
||||
`;
|
||||
expect(parser(value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "H1",
|
||||
},
|
||||
],
|
||||
"type": "heading-one",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "yo",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "list-item",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "bro",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "list-item",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "fro",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "list-item",
|
||||
},
|
||||
],
|
||||
"data": Object {
|
||||
"start": null,
|
||||
},
|
||||
"type": "bulleted-list",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should compile multiple header levels', () => {
|
||||
const value = `
|
||||
# H1
|
||||
|
||||
## H2
|
||||
|
||||
### H3
|
||||
`;
|
||||
expect(parser(value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "H1",
|
||||
},
|
||||
],
|
||||
"type": "heading-one",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "H2",
|
||||
},
|
||||
],
|
||||
"type": "heading-two",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "H3",
|
||||
},
|
||||
],
|
||||
"type": "heading-three",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should compile horizontal rules', () => {
|
||||
const value = `
|
||||
# H1
|
||||
|
||||
---
|
||||
|
||||
blue moon
|
||||
`;
|
||||
expect(parser(value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "H1",
|
||||
},
|
||||
],
|
||||
"type": "heading-one",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "",
|
||||
},
|
||||
],
|
||||
"type": "thematic-break",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "blue moon",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should compile horizontal rules', () => {
|
||||
const value = `
|
||||
# H1
|
||||
|
||||
---
|
||||
|
||||
blue moon
|
||||
`;
|
||||
expect(parser(value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "H1",
|
||||
},
|
||||
],
|
||||
"type": "heading-one",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "",
|
||||
},
|
||||
],
|
||||
"type": "thematic-break",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "blue moon",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should compile soft breaks (double space)', () => {
|
||||
const value = `
|
||||
blue moon
|
||||
footballs
|
||||
`;
|
||||
expect(parser(value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "blue moon",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "",
|
||||
},
|
||||
],
|
||||
"data": undefined,
|
||||
"type": "break",
|
||||
},
|
||||
Object {
|
||||
"text": "footballs",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should compile images', () => {
|
||||
const value = `
|
||||

|
||||
`;
|
||||
expect(parser(value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "",
|
||||
},
|
||||
],
|
||||
"data": Object {
|
||||
"alt": "super",
|
||||
"title": null,
|
||||
"url": "duper.jpg",
|
||||
},
|
||||
"type": "image",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should compile code blocks', () => {
|
||||
const value = `
|
||||
\`\`\`javascript
|
||||
var a = 1;
|
||||
\`\`\`
|
||||
`;
|
||||
expect(parser(value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "var a = 1;",
|
||||
},
|
||||
],
|
||||
"data": Object {
|
||||
"lang": "javascript",
|
||||
"shortcode": "code-block",
|
||||
"shortcodeData": Object {
|
||||
"code": "var a = 1;",
|
||||
"lang": "javascript",
|
||||
},
|
||||
},
|
||||
"type": "shortcode",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should compile nested inline markup', () => {
|
||||
const value = `
|
||||
# Word
|
||||
|
||||
This is **some *hot* content**
|
||||
|
||||
perhaps **scalding** even
|
||||
`;
|
||||
expect(parser(value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Word",
|
||||
},
|
||||
],
|
||||
"type": "heading-one",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "This is ",
|
||||
},
|
||||
Object {
|
||||
"bold": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "bold",
|
||||
},
|
||||
],
|
||||
"text": "some ",
|
||||
},
|
||||
Object {
|
||||
"bold": true,
|
||||
"italic": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "bold",
|
||||
},
|
||||
Object {
|
||||
"type": "italic",
|
||||
},
|
||||
],
|
||||
"text": "hot",
|
||||
},
|
||||
Object {
|
||||
"bold": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "bold",
|
||||
},
|
||||
],
|
||||
"text": " content",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "perhaps ",
|
||||
},
|
||||
Object {
|
||||
"bold": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "bold",
|
||||
},
|
||||
],
|
||||
"text": "scalding",
|
||||
},
|
||||
Object {
|
||||
"text": " even",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should compile inline code', () => {
|
||||
const value = `
|
||||
# Word
|
||||
|
||||
This is some sweet \`inline code\` yo!
|
||||
`;
|
||||
expect(parser(value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Word",
|
||||
},
|
||||
],
|
||||
"type": "heading-one",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "This is some sweet ",
|
||||
},
|
||||
Object {
|
||||
"code": true,
|
||||
"marks": Array [
|
||||
Object {
|
||||
"type": "code",
|
||||
},
|
||||
],
|
||||
"text": "inline code",
|
||||
},
|
||||
Object {
|
||||
"text": " yo!",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should compile links', () => {
|
||||
const value = `
|
||||
# Word
|
||||
|
||||
How far is it to [Google](https://google.com) land?
|
||||
`;
|
||||
expect(parser(value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Word",
|
||||
},
|
||||
],
|
||||
"type": "heading-one",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "How far is it to ",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "Google",
|
||||
},
|
||||
],
|
||||
"data": Object {
|
||||
"title": null,
|
||||
"url": "https://google.com",
|
||||
},
|
||||
"type": "link",
|
||||
},
|
||||
Object {
|
||||
"text": " land?",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should compile plugins', () => {
|
||||
const value = `
|
||||

|
||||
|
||||
{{< test >}}
|
||||
`;
|
||||
expect(parser(value)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "",
|
||||
},
|
||||
],
|
||||
"data": Object {
|
||||
"alt": "test",
|
||||
"title": null,
|
||||
"url": "test.png",
|
||||
},
|
||||
"type": "image",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"text": "{{< test >}}",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should compile kitchen sink example', () => {
|
||||
const value = `
|
||||
# An exhibit of Markdown
|
||||
|
||||
This note demonstrates some of what Markdown is capable of doing.
|
||||
|
||||
*Note: Feel free to play with this page. Unlike regular notes, this doesn't
|
||||
automatically save itself.*
|
||||
|
||||
## Basic formatting
|
||||
|
||||
Paragraphs can be written like so. A paragraph is the basic block of Markdown.
|
||||
A paragraph is what text will turn into when there is no reason it should
|
||||
become anything else.
|
||||
|
||||
Paragraphs must be separated by a blank line. Basic formatting of *italics* and
|
||||
**bold** is supported. This *can be **nested** like* so.
|
||||
|
||||
## Lists
|
||||
|
||||
### Ordered list
|
||||
|
||||
1. Item 1 2. A second item 3. Number 3 4. Ⅳ
|
||||
|
||||
*Note: the fourth item uses the Unicode character for Roman numeral four.*
|
||||
|
||||
### Unordered list
|
||||
|
||||
* An item Another item Yet another item And there's more...
|
||||
|
||||
## Paragraph modifiers
|
||||
|
||||
### Code block
|
||||
|
||||
Code blocks are very useful for developers and other people who look at
|
||||
code or other things that are written in plain text. As you can see, it
|
||||
uses a fixed-width font.
|
||||
|
||||
You can also make \`inline code\` to add code into other things.
|
||||
|
||||
### Quote
|
||||
|
||||
> Here is a quote. What this is should be self explanatory. Quotes are
|
||||
automatically indented when they are used.
|
||||
|
||||
## Headings
|
||||
|
||||
There are six levels of headings. They correspond with the six levels of HTML
|
||||
headings. You've probably noticed them already in the page. Each level down
|
||||
uses one more hash character.
|
||||
|
||||
### Headings *can* also contain **formatting**
|
||||
|
||||
### They can even contain \`inline code\`
|
||||
|
||||
Of course, demonstrating what headings look like messes up the structure of the
|
||||
page.
|
||||
|
||||
I don't recommend using more than three or four levels of headings here,
|
||||
because, when you're smallest heading isn't too small, and you're largest
|
||||
heading isn't too big, and you want each size up to look noticeably larger and
|
||||
more important, there there are only so many sizes that you can use.
|
||||
|
||||
## URLs
|
||||
|
||||
URLs can be made in a handful of ways:
|
||||
|
||||
* A named link to MarkItDown. The easiest way to do these is to select what you
|
||||
* want to make a link and hit \`Ctrl+L\`. Another named link to
|
||||
* [MarkItDown](http://www.markitdown.net/) Sometimes you just want a URL like
|
||||
* <http://www.markitdown.net/>.
|
||||
|
||||
## Horizontal rule
|
||||
|
||||
A horizontal rule is a line that goes across the middle of the page.
|
||||
|
||||
---
|
||||
|
||||
It's sometimes handy for breaking things up.
|
||||
|
||||
## Images
|
||||
|
||||
Markdown can also contain images. I'll need to add something here sometime.
|
||||
|
||||
## Finally
|
||||
|
||||
There's actually a lot more to Markdown than this. See the official
|
||||
introduction and syntax for more information. However, be aware that this is
|
||||
not using the official implementation, and this might work subtly differently
|
||||
in some of the little things.
|
||||
`;
|
||||
expect(parser(value)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React, { useState } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { fromJS } from 'immutable';
|
||||
import omit from 'lodash/omit';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { Range, Transforms } from 'slate';
|
||||
|
||||
import { getEditorControl, getEditorComponents } from '../index';
|
||||
|
||||
function Shortcode(props) {
|
||||
const editor = useSlate();
|
||||
const { element, dataKey = 'shortcodeData', children } = props;
|
||||
const EditorControl = getEditorControl();
|
||||
const plugin = getEditorComponents().get(element.data.shortcode);
|
||||
const fieldKeys = ['id', 'fromBlock', 'toBlock', 'toPreview', 'pattern', 'icon'];
|
||||
|
||||
const field = fromJS(omit(plugin, fieldKeys));
|
||||
const [value, setValue] = useState(fromJS(element.data[dataKey]));
|
||||
|
||||
function handleChange(fieldName, value, metadata) {
|
||||
const path = ReactEditor.findPath(editor, element);
|
||||
const newProperties = {
|
||||
data: {
|
||||
...element.data,
|
||||
[dataKey]: value.toJS(),
|
||||
metadata,
|
||||
},
|
||||
};
|
||||
Transforms.setNodes(editor, newProperties, {
|
||||
at: path,
|
||||
});
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
function handleFocus() {
|
||||
const path = ReactEditor.findPath(editor, element);
|
||||
Transforms.select(editor, path);
|
||||
}
|
||||
|
||||
const path = ReactEditor.findPath(editor, element);
|
||||
const isSelected =
|
||||
editor.selection &&
|
||||
path &&
|
||||
Range.isRange(editor.selection) &&
|
||||
Range.includes(editor.selection, path);
|
||||
|
||||
return (
|
||||
!field.isEmpty() && (
|
||||
<div onClick={handleFocus} onFocus={handleFocus}>
|
||||
<EditorControl
|
||||
css={css`
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
`}
|
||||
value={value}
|
||||
field={field}
|
||||
onChange={handleChange}
|
||||
isEditorComponent={true}
|
||||
onValidateObject={() => {}}
|
||||
isNewEditorComponent={element.data.shortcodeNew}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Shortcode;
|
||||
@@ -0,0 +1,58 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import { zIndex } from 'decap-cms-ui-default';
|
||||
import { ReactEditor, useSlate } from 'slate-react';
|
||||
import { Transforms } from 'slate';
|
||||
|
||||
import defaultEmptyBlock from '../plugins/blocks/defaultEmptyBlock';
|
||||
|
||||
function InsertionPoint(props) {
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
height: 32px;
|
||||
cursor: text;
|
||||
position: relative;
|
||||
z-index: ${zIndex.zIndex1};
|
||||
margin-top: -16px;
|
||||
`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function VoidBlock({ attributes, children, element }) {
|
||||
const editor = useSlate();
|
||||
const path = ReactEditor.findPath(editor, element);
|
||||
|
||||
function insertAtPath(at) {
|
||||
Transforms.insertNodes(editor, defaultEmptyBlock(), { select: true, at });
|
||||
}
|
||||
|
||||
function handleClick(event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
function handleInsertBefore() {
|
||||
insertAtPath(path);
|
||||
}
|
||||
|
||||
function handleInsertAfter() {
|
||||
insertAtPath([...path.slice(0, -1), path[path.length - 1] + 1]);
|
||||
}
|
||||
|
||||
const insertBefore = path[0] === 0;
|
||||
const nextElement = editor.children[path[0] + 1];
|
||||
const insertAfter = path[0] === editor.children.length - 1 || editor.isVoid(nextElement);
|
||||
|
||||
return (
|
||||
<div {...attributes} onClick={handleClick} contentEditable={false}>
|
||||
{insertBefore && <InsertionPoint onClick={handleInsertBefore} />}
|
||||
{children}
|
||||
{insertAfter && <InsertionPoint onClick={handleInsertAfter} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default VoidBlock;
|
||||
@@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { List, Map } from 'immutable';
|
||||
|
||||
import RawEditor from './RawEditor';
|
||||
import VisualEditor from './VisualEditor';
|
||||
|
||||
const MODE_STORAGE_KEY = 'cms.md-mode';
|
||||
|
||||
// TODO: passing the editorControl and components like this is horrible, should
|
||||
// be handled through Redux and a separate registry store for instances
|
||||
let editorControl;
|
||||
// eslint-disable-next-line func-style
|
||||
let _getEditorComponents = () => Map();
|
||||
|
||||
export function getEditorControl() {
|
||||
return editorControl;
|
||||
}
|
||||
|
||||
export function getEditorComponents() {
|
||||
return _getEditorComponents();
|
||||
}
|
||||
|
||||
export default class MarkdownControl extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onAddAsset: PropTypes.func.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
classNameWrapper: PropTypes.string.isRequired,
|
||||
editorControl: PropTypes.elementType.isRequired,
|
||||
value: PropTypes.string,
|
||||
field: ImmutablePropTypes.map.isRequired,
|
||||
getEditorComponents: PropTypes.func,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
value: '',
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
editorControl = props.editorControl;
|
||||
const preferredMode = localStorage.getItem(MODE_STORAGE_KEY) ?? 'rich_text';
|
||||
|
||||
_getEditorComponents = props.getEditorComponents;
|
||||
this.state = {
|
||||
mode:
|
||||
this.getAllowedModes().indexOf(preferredMode) !== -1
|
||||
? preferredMode
|
||||
: this.getAllowedModes()[0],
|
||||
pendingFocus: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Manually validate PropTypes - React 19 breaking change
|
||||
PropTypes.checkPropTypes(MarkdownControl.propTypes, this.props, 'prop', 'MarkdownControl');
|
||||
}
|
||||
|
||||
handleMode = mode => {
|
||||
this.setState({ mode, pendingFocus: true });
|
||||
localStorage.setItem(MODE_STORAGE_KEY, mode);
|
||||
};
|
||||
|
||||
processRef = ref => (this.ref = ref);
|
||||
|
||||
setFocusReceived = () => {
|
||||
this.setState({ pendingFocus: false });
|
||||
};
|
||||
|
||||
getAllowedModes = () => this.props.field.get('modes', List(['rich_text', 'raw'])).toArray();
|
||||
|
||||
focus() {
|
||||
this.setState({ pendingFocus: true });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
onChange,
|
||||
onAddAsset,
|
||||
getAsset,
|
||||
value,
|
||||
classNameWrapper,
|
||||
field,
|
||||
getEditorComponents,
|
||||
getRemarkPlugins,
|
||||
resolveWidget,
|
||||
t,
|
||||
isDisabled,
|
||||
} = this.props;
|
||||
|
||||
const { mode, pendingFocus } = this.state;
|
||||
const isShowModeToggle = this.getAllowedModes().length > 1;
|
||||
const visualEditor = (
|
||||
<div className="cms-editor-visual" ref={this.processRef}>
|
||||
<VisualEditor
|
||||
onChange={onChange}
|
||||
onAddAsset={onAddAsset}
|
||||
isShowModeToggle={isShowModeToggle}
|
||||
onMode={this.handleMode}
|
||||
getAsset={getAsset}
|
||||
className={classNameWrapper}
|
||||
value={value}
|
||||
field={field}
|
||||
getEditorComponents={getEditorComponents}
|
||||
getRemarkPlugins={getRemarkPlugins}
|
||||
resolveWidget={resolveWidget}
|
||||
pendingFocus={pendingFocus && this.setFocusReceived}
|
||||
t={t}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const rawEditor = (
|
||||
<div className="cms-editor-raw" ref={this.processRef}>
|
||||
<RawEditor
|
||||
onChange={onChange}
|
||||
onAddAsset={onAddAsset}
|
||||
isShowModeToggle={isShowModeToggle}
|
||||
onMode={this.handleMode}
|
||||
getAsset={getAsset}
|
||||
className={classNameWrapper}
|
||||
value={value}
|
||||
field={field}
|
||||
pendingFocus={pendingFocus && this.setFocusReceived}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
return mode === 'rich_text' ? visualEditor : rawEditor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import isHotkey from 'is-hotkey';
|
||||
|
||||
function BreakToDefaultBlock({ defaultType }) {
|
||||
return {
|
||||
onKeyDown(event, editor, next) {
|
||||
const { selection, startBlock } = editor.value;
|
||||
const isEnter = isHotkey('enter', event);
|
||||
if (!isEnter) {
|
||||
return next();
|
||||
}
|
||||
if (selection.isExpanded) {
|
||||
editor.delete();
|
||||
return next();
|
||||
}
|
||||
if (selection.start.isAtEndOfNode(startBlock) && startBlock.type !== defaultType) {
|
||||
return editor.insertBlock(defaultType);
|
||||
}
|
||||
return next();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default BreakToDefaultBlock;
|
||||
@@ -0,0 +1,25 @@
|
||||
import isHotkey from 'is-hotkey';
|
||||
|
||||
function CloseBlock({ defaultType }) {
|
||||
return {
|
||||
onKeyDown(event, editor, next) {
|
||||
const { selection, startBlock } = editor.value;
|
||||
const isBackspace = isHotkey('backspace', event);
|
||||
if (!isBackspace) {
|
||||
return next();
|
||||
}
|
||||
if (selection.isExpanded) {
|
||||
return editor.delete();
|
||||
}
|
||||
if (!selection.start.isAtStartOfNode(startBlock) || startBlock.text.length > 0) {
|
||||
return next();
|
||||
}
|
||||
if (startBlock.type !== defaultType) {
|
||||
editor.setBlocks(defaultType);
|
||||
}
|
||||
return next();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default CloseBlock;
|
||||
@@ -0,0 +1,158 @@
|
||||
import isArray from 'lodash/isArray';
|
||||
import tail from 'lodash/tail';
|
||||
import castArray from 'lodash/castArray';
|
||||
|
||||
function CommandsAndQueries({ defaultType }) {
|
||||
return {
|
||||
queries: {
|
||||
atStartOf(editor, node) {
|
||||
const { selection } = editor.value;
|
||||
return selection.isCollapsed && selection.start.isAtStartOfNode(node);
|
||||
},
|
||||
getAncestor(editor, firstKey, lastKey) {
|
||||
if (firstKey === lastKey) {
|
||||
return editor.value.document.getParent(firstKey);
|
||||
}
|
||||
return editor.value.document.getCommonAncestor(firstKey, lastKey);
|
||||
},
|
||||
getOffset(editor, node) {
|
||||
const parent = editor.value.document.getParent(node.key);
|
||||
return parent.nodes.indexOf(node);
|
||||
},
|
||||
getSelectedChildren(editor, node) {
|
||||
return node.nodes.filter(child => editor.isSelected(child));
|
||||
},
|
||||
getCommonAncestor(editor) {
|
||||
const { startBlock, endBlock, document: doc } = editor.value;
|
||||
return doc.getCommonAncestor(startBlock.key, endBlock.key);
|
||||
},
|
||||
getClosestType(editor, node, type) {
|
||||
const types = castArray(type);
|
||||
return editor.value.document.getClosest(node.key, n => types.includes(n.type));
|
||||
},
|
||||
getBlockContainer(editor, node) {
|
||||
const targetTypes = ['bulleted-list', 'numbered-list', 'list-item', 'quote', 'table-cell'];
|
||||
const { startBlock, selection } = editor.value;
|
||||
const target = node
|
||||
? editor.value.document.getParent(node.key)
|
||||
: (selection.isCollapsed && startBlock) || editor.getCommonAncestor();
|
||||
if (!target) {
|
||||
return editor.value.document;
|
||||
}
|
||||
if (targetTypes.includes(target.type)) {
|
||||
return target;
|
||||
}
|
||||
return editor.getBlockContainer(target);
|
||||
},
|
||||
isSelected(editor, nodes) {
|
||||
return castArray(nodes).every(node => {
|
||||
return editor.value.document.isInRange(node.key, editor.value.selection);
|
||||
});
|
||||
},
|
||||
isFirstChild(editor, node) {
|
||||
return editor.value.document.getParent(node.key).nodes.first().key === node.key;
|
||||
},
|
||||
areSiblings(editor, nodes) {
|
||||
if (!isArray(nodes) || nodes.length < 2) {
|
||||
return true;
|
||||
}
|
||||
const parent = editor.value.document.getParent(nodes[0].key);
|
||||
return tail(nodes).every(node => {
|
||||
return editor.value.document.getParent(node.key).key === parent.key;
|
||||
});
|
||||
},
|
||||
everyBlock(editor, type) {
|
||||
return editor.value.blocks.every(block => block.type === type);
|
||||
},
|
||||
hasMark(editor, type) {
|
||||
return editor.value.activeMarks.some(mark => mark.type === type);
|
||||
},
|
||||
hasBlock(editor, type) {
|
||||
return editor.value.blocks.some(node => node.type === type);
|
||||
},
|
||||
hasInline(editor, type) {
|
||||
return editor.value.inlines.some(node => node.type === type);
|
||||
},
|
||||
hasQuote(editor, quoteLabel) {
|
||||
const { value } = editor;
|
||||
const { document, blocks } = value;
|
||||
return blocks.some(node => {
|
||||
const { key: descendantNodeKey } = node;
|
||||
/* When focusing a quote block, the actual block that gets the focus is the paragraph block whose parent is a `quote` block.
|
||||
Hence, we need to get its parent and check if its type is `quote`. This parent will always be defined because every block in the editor
|
||||
has a Document object as parent by default.
|
||||
*/
|
||||
const parent = document.getParent(descendantNodeKey);
|
||||
return parent.type === quoteLabel;
|
||||
});
|
||||
},
|
||||
hasListItems(editor, listType) {
|
||||
const { value } = editor;
|
||||
const { document, blocks } = value;
|
||||
return blocks.some(node => {
|
||||
const { key: lowestNodeKey } = node;
|
||||
/* A list block has the following structure:
|
||||
<ol>
|
||||
<li>
|
||||
<p>Coffee</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Tea</p>
|
||||
</li>
|
||||
</ol>
|
||||
*/
|
||||
const parent = document.getParent(lowestNodeKey);
|
||||
const grandparent = document.getParent(parent.key);
|
||||
return parent.type === 'list-item' && grandparent?.type === listType;
|
||||
});
|
||||
},
|
||||
},
|
||||
commands: {
|
||||
toggleBlock(editor, type) {
|
||||
switch (type) {
|
||||
case 'heading-one':
|
||||
case 'heading-two':
|
||||
case 'heading-three':
|
||||
case 'heading-four':
|
||||
case 'heading-five':
|
||||
case 'heading-six':
|
||||
return editor.setBlocks(editor.everyBlock(type) ? defaultType : type);
|
||||
case 'quote':
|
||||
return editor.toggleQuoteBlock();
|
||||
case 'numbered-list':
|
||||
case 'bulleted-list': {
|
||||
return editor.toggleList(type);
|
||||
}
|
||||
}
|
||||
},
|
||||
unwrapBlockChildren(editor, block) {
|
||||
if (!block || block.object !== 'block') {
|
||||
throw Error(`Expected block but received ${block}.`);
|
||||
}
|
||||
const index = editor.value.document.getPath(block.key).last();
|
||||
const parent = editor.value.document.getParent(block.key);
|
||||
editor.withoutNormalizing(() => {
|
||||
block.nodes.forEach((node, idx) => {
|
||||
editor.moveNodeByKey(node.key, parent.key, index + idx);
|
||||
});
|
||||
editor.removeNodeByKey(block.key);
|
||||
});
|
||||
},
|
||||
unwrapNodeToDepth(editor, node, depth) {
|
||||
let currentDepth = 0;
|
||||
editor.withoutNormalizing(() => {
|
||||
while (currentDepth < depth) {
|
||||
editor.unwrapNodeByKey(node.key);
|
||||
currentDepth += 1;
|
||||
}
|
||||
});
|
||||
},
|
||||
unwrapNodeFromAncestor(editor, node, ancestor) {
|
||||
const depth = ancestor.getDepth(node.key);
|
||||
editor.unwrapNodeToDepth(node, depth);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default CommandsAndQueries;
|
||||
@@ -0,0 +1,38 @@
|
||||
function ForceInsert({ defaultType }) {
|
||||
return {
|
||||
queries: {
|
||||
canInsertBeforeNode(editor, node) {
|
||||
if (!editor.isVoid(node)) {
|
||||
return true;
|
||||
}
|
||||
return !!editor.value.document.getPreviousSibling(node.key);
|
||||
},
|
||||
canInsertAfterNode(editor, node) {
|
||||
if (!editor.isVoid(node)) {
|
||||
return true;
|
||||
}
|
||||
const nextSibling = editor.value.document.getNextSibling(node.key);
|
||||
return nextSibling && !editor.isVoid(nextSibling);
|
||||
},
|
||||
},
|
||||
commands: {
|
||||
forceInsertBeforeNode(editor, node) {
|
||||
const block = { type: defaultType, object: 'block' };
|
||||
const parent = editor.value.document.getParent(node.key);
|
||||
return editor.insertNodeByKey(parent.key, 0, block).moveToStartOfNode(parent).focus();
|
||||
},
|
||||
forceInsertAfterNode(editor, node) {
|
||||
return editor.moveToEndOfNode(node).insertBlock(defaultType).focus();
|
||||
},
|
||||
moveToEndOfDocument(editor) {
|
||||
const lastBlock = editor.value.document.nodes.last();
|
||||
if (editor.isVoid(lastBlock)) {
|
||||
editor.insertBlock(defaultType);
|
||||
}
|
||||
return editor.moveToEndOfNode(lastBlock).focus();
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default ForceInsert;
|
||||
@@ -0,0 +1,29 @@
|
||||
import isHotkey from 'is-hotkey';
|
||||
|
||||
export const HOT_KEY_MAP = {
|
||||
bold: 'mod+b',
|
||||
code: 'mod+shift+c',
|
||||
italic: 'mod+i',
|
||||
strikethrough: 'mod+shift+s',
|
||||
'heading-one': 'mod+1',
|
||||
'heading-two': 'mod+2',
|
||||
'heading-three': 'mod+3',
|
||||
'heading-four': 'mod+4',
|
||||
'heading-five': 'mod+5',
|
||||
'heading-six': 'mod+6',
|
||||
link: 'mod+k',
|
||||
};
|
||||
|
||||
function Hotkey(key, fn) {
|
||||
return {
|
||||
onKeyDown(event, editor, next) {
|
||||
if (!isHotkey(key, event)) {
|
||||
return next();
|
||||
}
|
||||
event.preventDefault();
|
||||
editor.command(fn);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default Hotkey;
|
||||
@@ -0,0 +1,15 @@
|
||||
import isHotkey from 'is-hotkey';
|
||||
|
||||
function LineBreak() {
|
||||
return {
|
||||
onKeyDown(event, editor, next) {
|
||||
const isShiftEnter = isHotkey('shift+enter', event);
|
||||
if (!isShiftEnter) {
|
||||
return next();
|
||||
}
|
||||
return editor.insertInline('break').insertText('').moveToStartOfNextText();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default LineBreak;
|
||||
@@ -0,0 +1,40 @@
|
||||
function Link({ type }) {
|
||||
return {
|
||||
commands: {
|
||||
toggleLink(editor, getUrl) {
|
||||
const selection = editor.value.selection;
|
||||
const isCollapsed = selection && selection.isCollapsed;
|
||||
|
||||
if (editor.hasInline(type)) {
|
||||
const inlines = editor.value.inlines.toJSON();
|
||||
const link = inlines.find(item => item.type === type);
|
||||
|
||||
const url = getUrl(link.data.url);
|
||||
|
||||
if (url) {
|
||||
// replace the old link
|
||||
return editor.setInlines({ data: { url } });
|
||||
} else {
|
||||
// remove url if it was removed by the user
|
||||
return editor.unwrapInline(type);
|
||||
}
|
||||
} else {
|
||||
const url = getUrl();
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
return isCollapsed
|
||||
? editor.insertInline({
|
||||
type,
|
||||
data: { url },
|
||||
nodes: [{ object: 'text', text: url }],
|
||||
})
|
||||
: editor.wrapInline({ type, data: { url } }).moveToEnd();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default Link;
|
||||
@@ -0,0 +1,8 @@
|
||||
function defaultEmptyBlock(text = '') {
|
||||
return {
|
||||
type: 'paragraph',
|
||||
children: [{ text }],
|
||||
};
|
||||
}
|
||||
|
||||
export default defaultEmptyBlock;
|
||||
@@ -0,0 +1,56 @@
|
||||
import isHotkey from 'is-hotkey';
|
||||
import { Editor, Transforms } from 'slate';
|
||||
|
||||
import keyDownEnter from './keyDownEnter';
|
||||
import keyDownBackspace from './keyDownBackspace';
|
||||
import isCursorInNonDefaultBlock from '../locations/isCursorInNonDefaultBlock';
|
||||
import toggleBlock from './toggleBlock';
|
||||
import isCursorCollapsedAfterSoftBreak from '../locations/isCursorCollapsedAfterSoftBreak';
|
||||
|
||||
const HEADING_HOTKEYS = {
|
||||
'mod+1': 'heading-one',
|
||||
'mod+2': 'heading-two',
|
||||
'mod+3': 'heading-three',
|
||||
'mod+4': 'heading-four',
|
||||
'mod+5': 'heading-five',
|
||||
'mod+6': 'heading-six',
|
||||
};
|
||||
|
||||
function keyDown(event, editor) {
|
||||
if (!editor.selection) return;
|
||||
|
||||
for (const hotkey in HEADING_HOTKEYS) {
|
||||
if (isHotkey(hotkey, event)) {
|
||||
toggleBlock(editor, HEADING_HOTKEYS[hotkey]);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHotkey('backspace', event) && isCursorCollapsedAfterSoftBreak(editor)) {
|
||||
const [, path] = Editor.previous(editor);
|
||||
Transforms.removeNodes(editor, { at: path });
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isCursorInNonDefaultBlock(editor)) return;
|
||||
|
||||
if (isHotkey('enter', event)) {
|
||||
const eventIntercepted = keyDownEnter(editor);
|
||||
if (eventIntercepted) {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHotkey('backspace', event)) {
|
||||
const eventIntercepted = keyDownBackspace(editor);
|
||||
if (eventIntercepted) {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default keyDown;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Transforms } from 'slate';
|
||||
|
||||
import unwrapIfCursorAtStart from '../transforms/unwrapIfCursorAtStart';
|
||||
import isCursorAtStartOfNonEmptyHeading from '../locations/isCursorAtStartOfNonEmptyHeading';
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
import areCurrentAndPreviousBlocksOfType from '../locations/areCurrentAndPreviousBlocksOfType';
|
||||
import isCursorAtStartOfBlockType from '../locations/isCursorAtStartOfBlockType';
|
||||
|
||||
function keyDownBackspace(editor) {
|
||||
if (!editor.selection) return;
|
||||
|
||||
if (isCursorAtStartOfNonEmptyHeading(editor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isCursorAtStartOfBlockType(editor, 'quote') &&
|
||||
areCurrentAndPreviousBlocksOfType(editor, 'quote')
|
||||
) {
|
||||
Transforms.mergeNodes(editor, lowestMatchedAncestor(editor, 'quote'));
|
||||
return true;
|
||||
}
|
||||
|
||||
return unwrapIfCursorAtStart(editor, true);
|
||||
}
|
||||
|
||||
export default keyDownBackspace;
|
||||
@@ -0,0 +1,26 @@
|
||||
import isCursorInBlockType from '../locations/isCursorInBlockType';
|
||||
import splitIntoParagraph from '../transforms/splitIntoParagraph';
|
||||
import unwrapIfCursorAtStart from '../transforms/unwrapIfCursorAtStart';
|
||||
import isCursorAtEndOfParagraph from '../locations/isCursorAtEndOfParagraph';
|
||||
|
||||
function keyDownEnter(editor) {
|
||||
if (!editor.selection) return;
|
||||
|
||||
if (isCursorInBlockType(editor, 'heading', true)) {
|
||||
return handleHeading(editor);
|
||||
}
|
||||
|
||||
return unwrapIfCursorAtStart(editor);
|
||||
}
|
||||
|
||||
function handleHeading(editor) {
|
||||
if (isCursorAtEndOfParagraph(editor)) {
|
||||
// split into paragraph if cursor is at the end of heading
|
||||
splitIntoParagraph(editor);
|
||||
return true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export default keyDownEnter;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Range, Transforms } from 'slate';
|
||||
|
||||
import isCursorInBlockType from '../locations/isCursorInBlockType';
|
||||
import getListTypeAtCursor from '../locations/getListTypeAtCursor';
|
||||
import wrapListItemsInBlock from '../transforms/wrapListItemsInBlock';
|
||||
|
||||
function toggleBlock(editor, type) {
|
||||
const { selection } = editor;
|
||||
if (!selection) return;
|
||||
|
||||
const isHeading = type.startsWith('heading-');
|
||||
const isActive = isCursorInBlockType(editor, type, isHeading, Range.isExpanded(selection));
|
||||
const listType = getListTypeAtCursor(editor);
|
||||
|
||||
// headings do not contain paragraphs so they could be converted, not wrapped/unwrapped
|
||||
if (isHeading) {
|
||||
Transforms.setNodes(editor, { type: isActive ? 'paragraph' : type });
|
||||
return;
|
||||
}
|
||||
|
||||
const { focus, anchor } = selection;
|
||||
if (
|
||||
!isActive &&
|
||||
listType &&
|
||||
focus.path[focus.path.length - 3] != anchor.path[anchor.path.length - 3]
|
||||
) {
|
||||
return wrapListItemsInBlock(editor, type, listType);
|
||||
}
|
||||
|
||||
if (!isActive) {
|
||||
return Transforms.wrapNodes(editor, { type });
|
||||
}
|
||||
|
||||
Transforms.unwrapNodes(editor, { match: n => n.type === type });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export default toggleBlock;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Editor } from 'slate';
|
||||
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
|
||||
function areCurrentAndPreviousBlocksOfType(editor, type) {
|
||||
const { selection } = editor;
|
||||
if (!selection) return false;
|
||||
|
||||
const [current] = Editor.nodes(editor, lowestMatchedAncestor(editor, 'block'));
|
||||
const previous = Editor.previous(editor, lowestMatchedAncestor(editor, type));
|
||||
|
||||
return current && previous && current[0].type === previous[0].type;
|
||||
}
|
||||
|
||||
export default areCurrentAndPreviousBlocksOfType;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Editor } from 'slate';
|
||||
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
|
||||
function getListTypeAtCursor(editor) {
|
||||
const list = Editor.above(editor, lowestMatchedAncestor(editor, 'list'));
|
||||
if (!list) return null;
|
||||
return list[0].type;
|
||||
}
|
||||
|
||||
export default getListTypeAtCursor;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Editor } from 'slate';
|
||||
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
|
||||
function isCursorAtEndOfParagraph(editor) {
|
||||
const { selection } = editor;
|
||||
if (!selection) return false;
|
||||
|
||||
const paragraph = Editor.above(editor, lowestMatchedAncestor(editor, 'paragraph'));
|
||||
|
||||
return !!paragraph && Editor.isEnd(editor, editor.selection.focus, paragraph[1]);
|
||||
}
|
||||
|
||||
export default isCursorAtEndOfParagraph;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Editor } from 'slate';
|
||||
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
|
||||
function isCursorAtStartOfBlockType(editor, type) {
|
||||
const { selection } = editor;
|
||||
if (!selection) return false;
|
||||
|
||||
const block = Editor.above(editor, lowestMatchedAncestor(editor, type));
|
||||
|
||||
return !!block && Editor.isStart(editor, editor.selection.focus, block[1]);
|
||||
}
|
||||
|
||||
export default isCursorAtStartOfBlockType;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Editor, Element } from 'slate';
|
||||
|
||||
function isCursorAtStartOfNonEmptyHeading(editor) {
|
||||
const { selection } = editor;
|
||||
if (!selection) return false;
|
||||
|
||||
const [match] = Array.from(
|
||||
Editor.nodes(editor, {
|
||||
match: n =>
|
||||
Element.isElement(n) && Editor.isBlock(editor, n) && `${n.type}`.startsWith('heading-'),
|
||||
mode: 'lowest',
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
!!match &&
|
||||
Editor.isStart(editor, editor.selection.focus, match[1]) &&
|
||||
!Editor.isEmpty(editor, match[0])
|
||||
);
|
||||
}
|
||||
|
||||
export default isCursorAtStartOfNonEmptyHeading;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Editor, Range } from 'slate';
|
||||
|
||||
function isCursorCollapsedAfterSoftBreak(editor) {
|
||||
const { selection } = editor;
|
||||
if (!selection) return false;
|
||||
if (Range.isExpanded(selection)) return false;
|
||||
|
||||
const previous = Editor.previous(editor);
|
||||
|
||||
return previous && previous[0].type == 'break';
|
||||
}
|
||||
|
||||
export default isCursorCollapsedAfterSoftBreak;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Editor, Element } from 'slate';
|
||||
|
||||
function isCursorInBlockType(editor, type, ignoreHeadings, ignoreLists) {
|
||||
const { selection } = editor;
|
||||
if (!selection) return false;
|
||||
|
||||
const [match] = Array.from(
|
||||
Editor.nodes(editor, {
|
||||
match: n =>
|
||||
Element.isElement(n) &&
|
||||
Editor.isBlock(editor, n) &&
|
||||
n.type !== 'paragraph' &&
|
||||
n.type !== 'list-item' &&
|
||||
(ignoreHeadings || !`${n.type}`.startsWith('heading-')) &&
|
||||
(!ignoreLists || !`${n.type}`.endsWith('-list')),
|
||||
mode: 'lowest',
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
!!match &&
|
||||
(match[0].type === type ||
|
||||
`${match[0].type}`.startsWith(`${type}-` || `${match[0].type}`.endsWith(`-${type}`)))
|
||||
);
|
||||
}
|
||||
|
||||
export default isCursorInBlockType;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Editor, Element } from 'slate';
|
||||
|
||||
function isCursorInNonDefaultBlock(editor) {
|
||||
const { selection } = editor;
|
||||
if (!selection) return false;
|
||||
|
||||
const [match] = Array.from(
|
||||
Editor.nodes(editor, {
|
||||
match: n => Element.isElement(n) && Editor.isBlock(editor, n) && n.type !== 'paragraph',
|
||||
mode: 'lowest',
|
||||
}),
|
||||
);
|
||||
|
||||
return !!match && !Editor.isEditor(match[0]);
|
||||
}
|
||||
|
||||
export default isCursorInNonDefaultBlock;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Editor, Transforms } from 'slate';
|
||||
|
||||
function splitIntoParagraph(editor) {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
Transforms.splitNodes(editor, { always: true });
|
||||
Transforms.setNodes(editor, { type: 'paragraph' });
|
||||
});
|
||||
|
||||
Editor.normalize(editor, { force: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
export default splitIntoParagraph;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Editor, Transforms } from 'slate';
|
||||
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
|
||||
function unwrapIfCursorAtStart(editor, mergeWithPrevious = false) {
|
||||
if (editor.selection.anchor.offset !== 0) return false;
|
||||
|
||||
let [node, path] = Editor.above(editor, lowestMatchedAncestor(editor, 'non-default'));
|
||||
|
||||
if (path.length == 0) return false;
|
||||
|
||||
const isHeading = `${node.type}`.startsWith('heading-');
|
||||
if (isHeading) {
|
||||
Transforms.setNodes(editor, { type: 'paragraph' });
|
||||
return false;
|
||||
}
|
||||
|
||||
const isBlock = Editor.isBlock(editor, node);
|
||||
const [parentBlock, parentBlockPath] = Editor.above(
|
||||
editor,
|
||||
lowestMatchedAncestor(editor, 'block'),
|
||||
);
|
||||
if (!isBlock) {
|
||||
if (!Editor.isStart(editor, path, parentBlockPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
[node, path] = [parentBlock, parentBlockPath];
|
||||
}
|
||||
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
Transforms.unwrapNodes(editor, { match: n => n.type === node.type, split: true });
|
||||
|
||||
if (mergeWithPrevious) {
|
||||
Transforms.mergeNodes(editor);
|
||||
}
|
||||
});
|
||||
|
||||
Editor.normalize(editor, { force: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
export default unwrapIfCursorAtStart;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Editor, Transforms } from 'slate';
|
||||
|
||||
function wrapListItemsInBlock(editor, blockType, listType) {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
Transforms.wrapNodes(editor, { type: listType });
|
||||
Transforms.wrapNodes(editor, { type: blockType }, { match: n => n.type === listType });
|
||||
Transforms.liftNodes(editor, { match: n => n.type === blockType });
|
||||
});
|
||||
Editor.normalize(editor, { force: true });
|
||||
}
|
||||
export default wrapListItemsInBlock;
|
||||
@@ -0,0 +1,15 @@
|
||||
import keyDown from './events/keyDown';
|
||||
import toggleBlock from './events/toggleBlock';
|
||||
|
||||
function withBlocks(editor) {
|
||||
if (editor.keyDownHandlers === undefined) {
|
||||
editor.keyDownHandlers = [];
|
||||
}
|
||||
editor.keyDownHandlers.push((event, editor) => keyDown(event, editor));
|
||||
|
||||
editor.toggleBlock = type => toggleBlock(editor, type);
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
export default withBlocks;
|
||||
@@ -0,0 +1,117 @@
|
||||
// source: https://github.com/ianstormtaylor/slate/blob/main/site/examples/ts/paste-html.tsx
|
||||
import { jsx } from 'slate-hyperscript';
|
||||
import { Transforms } from 'slate';
|
||||
|
||||
const ELEMENT_TAGS = {
|
||||
A: el => ({ type: 'link', url: el.getAttribute('href') }),
|
||||
BLOCKQUOTE: () => ({ type: 'quote' }),
|
||||
H1: () => ({ type: 'heading-one' }),
|
||||
H2: () => ({ type: 'heading-two' }),
|
||||
H3: () => ({ type: 'heading-three' }),
|
||||
H4: () => ({ type: 'heading-four' }),
|
||||
H5: () => ({ type: 'heading-five' }),
|
||||
H6: () => ({ type: 'heading-six' }),
|
||||
IMG: el => ({ type: 'image', url: el.getAttribute('src') }),
|
||||
LI: () => ({ type: 'list-item' }),
|
||||
OL: () => ({ type: 'numbered-list' }),
|
||||
P: () => ({ type: 'paragraph' }),
|
||||
PRE: () => ({ type: 'code' }),
|
||||
UL: () => ({ type: 'bulleted-list' }),
|
||||
};
|
||||
|
||||
// COMPAT: `B` is omitted here because Google Docs uses `<b>` in weird ways.
|
||||
const TEXT_TAGS = {
|
||||
CODE: () => ({ code: true }),
|
||||
DEL: () => ({ strikethrough: true }),
|
||||
EM: () => ({ italic: true }),
|
||||
I: () => ({ italic: true }),
|
||||
S: () => ({ strikethrough: true }),
|
||||
STRONG: () => ({ bold: true }),
|
||||
U: () => ({ underline: true }),
|
||||
};
|
||||
|
||||
const INLINE_STYLES = {
|
||||
'font-style': value => (value === 'italic' ? { italic: true } : {}),
|
||||
'font-weight': value => (value === 'bold' || parseInt(value, 10) >= 600 ? { bold: true } : {}),
|
||||
};
|
||||
|
||||
function deserialize(el) {
|
||||
if (el.nodeType === 3) {
|
||||
return el.textContent;
|
||||
} else if (el.nodeType !== 1) {
|
||||
return null;
|
||||
} else if (el.nodeName === 'BR') {
|
||||
return '\n';
|
||||
}
|
||||
|
||||
const { nodeName } = el;
|
||||
let parent = el;
|
||||
|
||||
if (nodeName === 'PRE' && el.childNodes[0] && el.childNodes[0].nodeName === 'CODE') {
|
||||
parent = el.childNodes[0];
|
||||
}
|
||||
let children = Array.from(parent.childNodes).map(deserialize).flat();
|
||||
|
||||
if (children.length === 0) {
|
||||
children = [{ text: '' }];
|
||||
}
|
||||
|
||||
if (el.nodeName === 'BODY') {
|
||||
return jsx('fragment', {}, children);
|
||||
}
|
||||
|
||||
if (ELEMENT_TAGS[nodeName]) {
|
||||
const attrs = ELEMENT_TAGS[nodeName](el);
|
||||
return jsx('element', attrs, children);
|
||||
}
|
||||
|
||||
if (TEXT_TAGS[nodeName]) {
|
||||
const attrs = TEXT_TAGS[nodeName](el);
|
||||
return children.map(child => jsx('text', attrs, child));
|
||||
}
|
||||
|
||||
// Convert inline CSS on span elements generated by Google Docs
|
||||
if (nodeName === 'SPAN') {
|
||||
const attrs = {};
|
||||
for (let i = 0; i < el.style.length; i++) {
|
||||
const propertyName = el.style[i];
|
||||
if (INLINE_STYLES[propertyName]) {
|
||||
const propertyValue = el.style.getPropertyValue(propertyName);
|
||||
const propertyStyle = INLINE_STYLES[propertyName](propertyValue);
|
||||
Object.assign(attrs, propertyStyle);
|
||||
}
|
||||
}
|
||||
return children.map(child => jsx('text', attrs, child));
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function withHtml(editor) {
|
||||
const { insertData, isInline, isVoid } = editor;
|
||||
|
||||
editor.isInline = element => {
|
||||
return element.type === 'link' ? true : isInline(element);
|
||||
};
|
||||
|
||||
editor.isVoid = element => {
|
||||
return element.type === 'image' ? true : isVoid(element);
|
||||
};
|
||||
|
||||
editor.insertData = data => {
|
||||
const html = data.getData('text/html');
|
||||
|
||||
if (html) {
|
||||
const parsed = new DOMParser().parseFromString(html, 'text/html');
|
||||
const fragment = deserialize(parsed.body);
|
||||
Transforms.insertFragment(editor, fragment);
|
||||
return;
|
||||
}
|
||||
|
||||
insertData(data);
|
||||
};
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
export default withHtml;
|
||||
@@ -0,0 +1,38 @@
|
||||
import isHotkey from 'is-hotkey';
|
||||
|
||||
import toggleMark from './toggleMark';
|
||||
import keyDownShiftEnter from './keyDownShiftEnter';
|
||||
import toggleLink from './toggleLink';
|
||||
|
||||
const MARK_HOTKEYS = {
|
||||
'mod+b': 'bold',
|
||||
'mod+i': 'italic',
|
||||
'mod+u': 'underline',
|
||||
'mod+`': 'code',
|
||||
'mod+shift+s': 'delete',
|
||||
'mod+shift+c': 'code',
|
||||
};
|
||||
|
||||
function keyDown(event, editor) {
|
||||
if (!editor.selection) return;
|
||||
|
||||
for (const hotkey in MARK_HOTKEYS) {
|
||||
if (isHotkey(hotkey, event)) {
|
||||
toggleMark(editor, MARK_HOTKEYS[hotkey]);
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHotkey('mod+k', event)) {
|
||||
event.preventDefault();
|
||||
return toggleLink(editor);
|
||||
}
|
||||
|
||||
if (isHotkey('shift+enter', event)) {
|
||||
event.preventDefault();
|
||||
return keyDownShiftEnter(editor);
|
||||
}
|
||||
}
|
||||
|
||||
export default keyDown;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Editor, Transforms } from 'slate';
|
||||
|
||||
function keyDownShiftEnter(editor) {
|
||||
if (!editor.selection) return;
|
||||
|
||||
const focus = {
|
||||
path: [
|
||||
...editor.selection.focus.path.slice(0, -1),
|
||||
editor.selection.focus.path[editor.selection.focus.path.length - 1] + 2,
|
||||
],
|
||||
offset: 0,
|
||||
};
|
||||
|
||||
Transforms.insertNodes(editor, {
|
||||
type: 'break',
|
||||
children: [
|
||||
{
|
||||
text: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
Editor.normalize(editor, { force: true });
|
||||
|
||||
Transforms.select(editor, focus);
|
||||
return false;
|
||||
}
|
||||
|
||||
export default keyDownShiftEnter;
|
||||
@@ -0,0 +1,17 @@
|
||||
import getActiveLink from '../selectors/getActiveLink';
|
||||
import unwrapLink from '../transforms/unwrapLink';
|
||||
import wrapLink from '../transforms/wrapLink';
|
||||
|
||||
function toggleLink(editor, promptText) {
|
||||
const activeLink = getActiveLink(editor);
|
||||
const activeUrl = activeLink ? activeLink[0]?.data?.url : '';
|
||||
const url = window.prompt(promptText, activeUrl);
|
||||
if (url == null) return;
|
||||
if (url === '') {
|
||||
unwrapLink(editor);
|
||||
return;
|
||||
}
|
||||
wrapLink(editor, url);
|
||||
}
|
||||
|
||||
export default toggleLink;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Editor } from 'slate';
|
||||
|
||||
import isMarkActive from '../locations/isMarkActive';
|
||||
|
||||
function toggleMark(editor, format) {
|
||||
if (isMarkActive(editor, format)) {
|
||||
Editor.removeMark(editor, format);
|
||||
} else {
|
||||
Editor.addMark(editor, format, true);
|
||||
}
|
||||
}
|
||||
|
||||
export default toggleMark;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Editor } from 'slate';
|
||||
|
||||
function isMarkActive(editor, format) {
|
||||
const { selection } = editor;
|
||||
if (!selection) return false;
|
||||
|
||||
const marks = Editor.marks(editor);
|
||||
return marks ? marks[format] === true : false;
|
||||
}
|
||||
|
||||
export default isMarkActive;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Editor } from 'slate';
|
||||
|
||||
import matchLink from '../../matchers/matchLink';
|
||||
|
||||
function getActiveLink(editor) {
|
||||
const [link] = Editor.nodes(editor, matchLink(editor));
|
||||
return link;
|
||||
}
|
||||
|
||||
export default getActiveLink;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Transforms } from 'slate';
|
||||
|
||||
import matchLink from '../../matchers/matchLink';
|
||||
|
||||
function unwrapLink(editor) {
|
||||
Transforms.unwrapNodes(editor, matchLink());
|
||||
}
|
||||
|
||||
export default unwrapLink;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Range, Transforms } from 'slate';
|
||||
|
||||
import getActiveLink from '../selectors/getActiveLink';
|
||||
import matchLink from '../../matchers/matchLink';
|
||||
|
||||
function wrapLink(editor, url) {
|
||||
if (getActiveLink(editor)) {
|
||||
Transforms.setNodes(editor, { data: { url } }, matchLink());
|
||||
return;
|
||||
}
|
||||
|
||||
const { selection } = editor;
|
||||
const isCollapsed = selection && Range.isCollapsed(selection);
|
||||
const link = {
|
||||
type: 'link',
|
||||
data: {
|
||||
url,
|
||||
},
|
||||
children: isCollapsed ? [{ text: url }] : [],
|
||||
};
|
||||
|
||||
if (isCollapsed) {
|
||||
Transforms.insertNodes(editor, link);
|
||||
} else {
|
||||
Transforms.wrapNodes(editor, link, { split: true });
|
||||
Transforms.collapse(editor, { edge: 'end' });
|
||||
}
|
||||
}
|
||||
|
||||
export default wrapLink;
|
||||
@@ -0,0 +1,20 @@
|
||||
import keyDown from './events/keyDown';
|
||||
|
||||
function withInlines(editor) {
|
||||
const { isInline, isVoid } = editor;
|
||||
|
||||
editor.isInline = element =>
|
||||
['link', 'button', 'break', 'image'].includes(element.type) || isInline(element);
|
||||
|
||||
editor.isVoid = element =>
|
||||
['break', 'image', 'thematic-break'].includes(element.type) || isVoid(element);
|
||||
|
||||
if (editor.keyDownHandlers === undefined) {
|
||||
editor.keyDownHandlers = [];
|
||||
}
|
||||
editor.keyDownHandlers.push((event, editor) => keyDown(event, editor));
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
export default withInlines;
|
||||
@@ -0,0 +1,34 @@
|
||||
import isHotkey from 'is-hotkey';
|
||||
|
||||
import keyDownEnter from './keyDownEnter';
|
||||
import keyDownTab from './keyDownTab';
|
||||
import keyDownShiftTab from './keyDownShiftTab';
|
||||
import keyDownBackspace from './keyDownBackspace';
|
||||
|
||||
function keyDown(event, editor) {
|
||||
if (!editor.isListItem()) return;
|
||||
|
||||
if (isHotkey('enter', event)) {
|
||||
event.preventDefault();
|
||||
keyDownEnter(editor);
|
||||
return false;
|
||||
}
|
||||
if (isHotkey('backspace', event)) {
|
||||
const eventIntercepted = keyDownBackspace(editor);
|
||||
if (eventIntercepted === false) {
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isHotkey('tab', event)) {
|
||||
event.preventDefault();
|
||||
return keyDownTab(editor);
|
||||
}
|
||||
if (isHotkey('shift+tab', event)) {
|
||||
event.preventDefault();
|
||||
return keyDownShiftTab(editor);
|
||||
}
|
||||
}
|
||||
|
||||
export default keyDown;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Range } from 'slate';
|
||||
|
||||
import isCursorInListItem from '../locations/isCursorInListItem';
|
||||
import isSelectionWithinNoninitialListItem from '../locations/isSelectionWithinNoninitialListItem';
|
||||
import unwrapSelectionFromList from '../transforms/unwrapSelectionFromList';
|
||||
import mergeWithPreviousListItem from '../transforms/mergeWithPreviousListItem';
|
||||
import isCursorAtNoninitialParagraphStart from '../locations/isCursorAtNoninitialParagraphStart';
|
||||
|
||||
function keyDownBackspace(editor) {
|
||||
if (!editor.selection) return;
|
||||
|
||||
// ignore if selection is expanded, cursor is not at the beginning or not immediately in a list item, or cursor is at the beginning of a non-initial paragraph
|
||||
if (
|
||||
!Range.isCollapsed(editor.selection) ||
|
||||
editor.selection.anchor.offset !== 0 ||
|
||||
!isCursorInListItem(editor, true) ||
|
||||
isCursorAtNoninitialParagraphStart(editor)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelectionWithinNoninitialListItem(editor)) {
|
||||
mergeWithPreviousListItem(editor);
|
||||
} else {
|
||||
unwrapSelectionFromList(editor);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export default keyDownBackspace;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Range, Transforms } from 'slate';
|
||||
|
||||
import splitListItem from '../transforms/splitListItem';
|
||||
import isCursorAtListItemStart from '../locations/isCursorAtListItemStart';
|
||||
import liftListItem from '../transforms/liftListItem';
|
||||
import convertParagraphToListItem from '../transforms/convertParagraphToListItem';
|
||||
import isCursorAtNoninitialParagraphStart from '../locations/isCursorAtNoninitialParagraphStart';
|
||||
import splitToNestedList from '../transforms/splitToNestedList';
|
||||
import getListContainedInListItem from '../selectors/getListContainedInListItem';
|
||||
|
||||
function keyDownEnter(editor) {
|
||||
if (!editor.selection) return;
|
||||
|
||||
// Pressing enter will delete current selection in any case
|
||||
if (Range.isExpanded(editor.selection)) {
|
||||
Transforms.delete(editor);
|
||||
}
|
||||
|
||||
// if edge of selection is in the beginning of the first text node in list-item
|
||||
if (isCursorAtListItemStart(editor)) {
|
||||
return liftListItem(editor);
|
||||
}
|
||||
|
||||
// if list has a nested list, insert new item to the beginning of the nested list
|
||||
const nestedList = getListContainedInListItem(editor);
|
||||
if (!!nestedList && `${nestedList[0].type}`.endsWith('-list')) {
|
||||
return splitToNestedList(editor, nestedList[0].type);
|
||||
}
|
||||
|
||||
// if a paragraph in a list and has previous siblings, convert it to a list item
|
||||
if (isCursorAtNoninitialParagraphStart(editor)) {
|
||||
return convertParagraphToListItem(editor);
|
||||
}
|
||||
|
||||
// otherwise create a new list item
|
||||
splitListItem(editor);
|
||||
}
|
||||
|
||||
export default keyDownEnter;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Editor, Transforms } from 'slate';
|
||||
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
import matchedAncestors from '../../matchers/matchedAncestors';
|
||||
|
||||
function keyDownShiftTab(editor) {
|
||||
if (!editor.selection) return;
|
||||
|
||||
if (Array.from(Editor.nodes(editor, matchedAncestors(editor, 'list'))).length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
Transforms.liftNodes(editor, {
|
||||
...lowestMatchedAncestor(editor, 'list-item'),
|
||||
split: true,
|
||||
});
|
||||
Transforms.liftNodes(editor, {
|
||||
...lowestMatchedAncestor(editor, 'list-item'),
|
||||
split: true,
|
||||
});
|
||||
});
|
||||
|
||||
Editor.normalize(editor);
|
||||
}
|
||||
|
||||
export default keyDownShiftTab;
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Editor, Transforms } from 'slate';
|
||||
|
||||
import isSelectionWithinNoninitialListItem from '../locations/isSelectionWithinNoninitialListItem';
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
import moveListToListItem from '../transforms/moveListToListItem';
|
||||
|
||||
function keyDownTab(editor) {
|
||||
if (!editor.selection) return;
|
||||
|
||||
if (!isSelectionWithinNoninitialListItem(editor)) return;
|
||||
|
||||
// In a case where one edge of the range is within a nested list item, we need to even the selection to the outer most level
|
||||
const { focus, anchor } = editor.selection;
|
||||
|
||||
const pathLength =
|
||||
focus.path.length > anchor.path.length ? anchor.path.length : focus.path.length;
|
||||
const at = {
|
||||
anchor: {
|
||||
offset: 0,
|
||||
path: [...anchor.path.slice(0, pathLength - 2), 0, 0],
|
||||
},
|
||||
focus: {
|
||||
offset: 0,
|
||||
path: [...focus.path.slice(0, pathLength - 2), 0, 0],
|
||||
},
|
||||
};
|
||||
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
// wrap selected list items into a new bulleted list
|
||||
Transforms.wrapNodes(
|
||||
editor,
|
||||
{
|
||||
type: 'bulleted-list',
|
||||
},
|
||||
{
|
||||
...lowestMatchedAncestor(editor, 'list-item'),
|
||||
at,
|
||||
},
|
||||
);
|
||||
|
||||
// get the new bulleted list position
|
||||
const [, newListPath] = Editor.above(editor, lowestMatchedAncestor(editor, 'list'));
|
||||
|
||||
// get the new parent node (previous list item)
|
||||
const parentNode = Editor.previous(editor, {
|
||||
at: newListPath,
|
||||
});
|
||||
|
||||
moveListToListItem(editor, newListPath, parentNode);
|
||||
});
|
||||
|
||||
Editor.normalize(editor);
|
||||
}
|
||||
|
||||
export default keyDownTab;
|
||||
@@ -0,0 +1,23 @@
|
||||
import isCursorInListItem from '../locations/isCursorInListItem';
|
||||
import getLowestAncestorList from '../selectors/getLowestAncestorList';
|
||||
import wrapSelectionInList from '../transforms/wrapSelectionInList';
|
||||
import changeListType from '../transforms/changeListType';
|
||||
import unwrapSelectionFromList from '../transforms/unwrapSelectionFromList';
|
||||
|
||||
function toggleListType(editor, type) {
|
||||
// list being active means that we are in a paragraph or heading whose parent is a list
|
||||
// if no list is active, wrap selection in a new list of the given type
|
||||
if (!isCursorInListItem(editor)) {
|
||||
return wrapSelectionInList(editor, type);
|
||||
}
|
||||
// if a list is active but the type doesn't match, change selection to the given list type
|
||||
const currentList = getLowestAncestorList(editor);
|
||||
if (currentList && currentList[0].type !== type) {
|
||||
return changeListType(editor, type);
|
||||
}
|
||||
|
||||
// if a list is active and the type matches, unwrap selection from the list
|
||||
return unwrapSelectionFromList(editor);
|
||||
}
|
||||
|
||||
export default toggleListType;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Range } from 'slate';
|
||||
|
||||
function isCursorAtListItemStart(editor) {
|
||||
if (!editor.selection) return false;
|
||||
|
||||
const { offset, path } = Range.start(editor.selection);
|
||||
// todo: this will break when there are marks inside list items, use Edior.isStart on first block parent instead (see isCursorAtEndOfParagraph)
|
||||
return (
|
||||
offset === 0 && path.length >= 2 && path[path.length - 1] === 0 && path[path.length - 2] === 0
|
||||
);
|
||||
}
|
||||
|
||||
export default isCursorAtListItemStart;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Range } from 'slate';
|
||||
|
||||
function isCursorAtNoninitialParagraphStart(editor) {
|
||||
if (!editor.selection) return false;
|
||||
|
||||
const { offset, path } = Range.start(editor.selection);
|
||||
|
||||
return offset == 0 && path.length > 2 && path[path.length - 2] > 0;
|
||||
}
|
||||
|
||||
export default isCursorAtNoninitialParagraphStart;
|
||||
@@ -0,0 +1,9 @@
|
||||
import getListContainedInListItem from '../selectors/getListContainedInListItem';
|
||||
|
||||
function isCursorInItemContainingNestedList(editor) {
|
||||
const nestedList = getListContainedInListItem(editor);
|
||||
|
||||
return !!nestedList && `${nestedList[0].type}`.endsWith('-list');
|
||||
}
|
||||
|
||||
export default isCursorInItemContainingNestedList;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Editor, Element } from 'slate';
|
||||
|
||||
function isCursorInListItem(editor, immediate) {
|
||||
const { selection } = editor;
|
||||
if (!selection) return false;
|
||||
|
||||
const [match] = Array.from(
|
||||
Editor.nodes(editor, {
|
||||
match: n =>
|
||||
Element.isElement(n) &&
|
||||
Editor.isBlock(editor, n) &&
|
||||
n.type !== 'paragraph' &&
|
||||
(immediate || !`${n.type}`.startsWith('heading-')),
|
||||
mode: 'lowest',
|
||||
}),
|
||||
);
|
||||
|
||||
return !!match && match[0].type === 'list-item';
|
||||
}
|
||||
|
||||
export default isCursorInListItem;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Editor, Range } from 'slate';
|
||||
|
||||
function isSelectionWithinNoninitialListItem(editor) {
|
||||
if (!editor.selection) return false;
|
||||
|
||||
const [, path] = Editor.above(editor, {
|
||||
match: n => n.type === 'list-item',
|
||||
mode: 'lowest',
|
||||
at: Range.start(editor.selection),
|
||||
});
|
||||
if (path && path.length > 0 && path[path.length - 1] > 0) return true;
|
||||
}
|
||||
|
||||
export default isSelectionWithinNoninitialListItem;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Editor } from 'slate';
|
||||
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
|
||||
function getListContainedInListItem(editor) {
|
||||
if (!editor.selection) return false;
|
||||
|
||||
const [, paragraphPath] = Editor.above(editor, lowestMatchedAncestor(editor, 'paragraph'));
|
||||
return Editor.next(editor, { at: paragraphPath });
|
||||
}
|
||||
|
||||
export default getListContainedInListItem;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Editor } from 'slate';
|
||||
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
|
||||
function getLowestAncestorList(editor) {
|
||||
if (!editor.selection) return false;
|
||||
|
||||
return Editor.above(editor, lowestMatchedAncestor(editor, 'list'));
|
||||
}
|
||||
|
||||
export default getLowestAncestorList;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Editor } from 'slate';
|
||||
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
|
||||
function getLowestAncestorQuote(editor) {
|
||||
if (!editor.selection) return false;
|
||||
|
||||
return Editor.above(editor, lowestMatchedAncestor(editor, 'quote'));
|
||||
}
|
||||
|
||||
export default getLowestAncestorQuote;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Editor, Transforms } from 'slate';
|
||||
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
|
||||
function changeListType(editor, type) {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
// wrap selected list items into new type
|
||||
Transforms.wrapNodes(editor, { type }, lowestMatchedAncestor(editor, 'list-item'));
|
||||
// lift the new list of the current list, split if necessary
|
||||
Transforms.liftNodes(editor, lowestMatchedAncestor(editor, type));
|
||||
});
|
||||
|
||||
Editor.normalize(editor, { force: true });
|
||||
}
|
||||
|
||||
export default changeListType;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Editor } from 'slate';
|
||||
|
||||
import liftFirstMatchedParent from './liftFirstMatchedParent';
|
||||
import wrapFirstMatchedParent from './wrapFirstMatchedParent';
|
||||
|
||||
function convertParagraphToListItem(editor) {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
// wrap the paragraph in a list item
|
||||
wrapFirstMatchedParent(editor, 'paragraph', {
|
||||
type: 'list-item',
|
||||
});
|
||||
// lift the new list-item of the current list-item, split if necessary
|
||||
liftFirstMatchedParent(editor, 'list-item', { split: true });
|
||||
});
|
||||
|
||||
Editor.normalize(editor, { force: true });
|
||||
}
|
||||
|
||||
export default convertParagraphToListItem;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Transforms } from 'slate';
|
||||
|
||||
function liftFirstMatchedParent(editor, format, options) {
|
||||
Transforms.liftNodes(editor, {
|
||||
match: n => n.type === format || (format === 'paragraph' && `${n.type}`.startsWith('heading')),
|
||||
mode: 'lowest',
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export default liftFirstMatchedParent;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Editor } from 'slate';
|
||||
|
||||
import unwrapFirstMatchedParent from './unwrapFirstMatchedParent';
|
||||
import liftFirstMatchedParent from './liftFirstMatchedParent';
|
||||
import getLowestAncestorList from '../selectors/getLowestAncestorList';
|
||||
import getLowestAncestorQuote from '../selectors/getLowestAncestorQuote';
|
||||
|
||||
function liftListItem(editor) {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
// lift the paragraph out of the list and split if necessary
|
||||
liftFirstMatchedParent(editor, 'list-item', { split: true });
|
||||
|
||||
// if list is nested and not wrapped in quote, lift into the parent list, unwrap otherwise
|
||||
const parentList = getLowestAncestorList(editor);
|
||||
const parentQuote = getLowestAncestorQuote(editor);
|
||||
if (
|
||||
(parentList && !parentQuote) ||
|
||||
(parentList && parentQuote && parentList[1].length > parentQuote[1].length)
|
||||
) {
|
||||
liftFirstMatchedParent(editor, 'list-item', { split: true });
|
||||
} else {
|
||||
// unwrap the paragraph from list-item element
|
||||
unwrapFirstMatchedParent(editor, 'list-item');
|
||||
}
|
||||
});
|
||||
|
||||
Editor.normalize(editor, { force: true });
|
||||
}
|
||||
|
||||
export default liftListItem;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Editor, Transforms } from 'slate';
|
||||
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
|
||||
function mergeWithPreviousListItem(editor) {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
Transforms.mergeNodes(editor, lowestMatchedAncestor(editor, 'list-item'));
|
||||
});
|
||||
|
||||
Editor.normalize(editor, { force: true });
|
||||
}
|
||||
|
||||
export default mergeWithPreviousListItem;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Transforms } from 'slate';
|
||||
|
||||
function moveListToListItem(editor, listPath, targetListItem) {
|
||||
const [targetItem, targetPath] = targetListItem;
|
||||
|
||||
// move the node to the last child position of the parent node
|
||||
Transforms.moveNodes(editor, {
|
||||
at: listPath,
|
||||
to: [...targetPath, targetItem.children.length],
|
||||
});
|
||||
}
|
||||
|
||||
export default moveListToListItem;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Editor, Range, Transforms } from 'slate';
|
||||
|
||||
import liftFirstMatchedParent from './liftFirstMatchedParent';
|
||||
import wrapFirstMatchedParent from './wrapFirstMatchedParent';
|
||||
|
||||
function splitListItem(editor) {
|
||||
if (!editor.selection) return false;
|
||||
|
||||
if (Range.isExpanded(editor.selection)) {
|
||||
Transforms.delete(editor);
|
||||
}
|
||||
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
// split even if at the end of current text
|
||||
Transforms.splitNodes(editor, {
|
||||
always: true,
|
||||
});
|
||||
// set the new node to paragraph (to avoid splitting headings)
|
||||
Transforms.setNodes(editor, {
|
||||
type: 'paragraph',
|
||||
});
|
||||
// wrap the paragraph in a list item
|
||||
wrapFirstMatchedParent(editor, 'paragraph', {
|
||||
type: 'list-item',
|
||||
});
|
||||
// lift new list item out the paragraph
|
||||
liftFirstMatchedParent(editor, 'list-item');
|
||||
});
|
||||
|
||||
Editor.normalize(editor, { force: true });
|
||||
}
|
||||
|
||||
export default splitListItem;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Editor, Range, Transforms } from 'slate';
|
||||
|
||||
import wrapFirstMatchedParent from './wrapFirstMatchedParent';
|
||||
|
||||
function splitToNestedList(editor, listType) {
|
||||
if (!editor.selection) return false;
|
||||
|
||||
if (Range.isExpanded(editor.selection)) {
|
||||
Transforms.delete(editor);
|
||||
}
|
||||
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
// split even if at the end of current text
|
||||
Transforms.splitNodes(editor, {
|
||||
always: true,
|
||||
});
|
||||
// set the new node to paragraph (to avoid splitting headings)
|
||||
Transforms.setNodes(editor, {
|
||||
type: 'paragraph',
|
||||
});
|
||||
// wrap the paragraph in a list item
|
||||
wrapFirstMatchedParent(editor, 'paragraph', {
|
||||
type: 'list-item',
|
||||
});
|
||||
wrapFirstMatchedParent(editor, 'list-item', {
|
||||
type: listType,
|
||||
});
|
||||
});
|
||||
|
||||
Editor.normalize(editor, { force: true });
|
||||
}
|
||||
|
||||
export default splitToNestedList;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Transforms } from 'slate';
|
||||
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
|
||||
function unwrapFirstMatchedParent(editor, format) {
|
||||
Transforms.unwrapNodes(editor, lowestMatchedAncestor(editor, format));
|
||||
}
|
||||
|
||||
export default unwrapFirstMatchedParent;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Editor, Transforms } from 'slate';
|
||||
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
|
||||
function unwrapSelectionFromList(editor) {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
Transforms.unwrapNodes(editor, { ...lowestMatchedAncestor(editor, 'list'), split: true });
|
||||
Transforms.unwrapNodes(editor, lowestMatchedAncestor(editor, 'list-item'));
|
||||
});
|
||||
|
||||
Editor.normalize(editor);
|
||||
}
|
||||
|
||||
export default unwrapSelectionFromList;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Transforms } from 'slate';
|
||||
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
|
||||
function wrapFirstMatchedParent(editor, format, node) {
|
||||
Transforms.wrapNodes(editor, node, lowestMatchedAncestor(editor, format));
|
||||
}
|
||||
|
||||
export default wrapFirstMatchedParent;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Editor, Transforms } from 'slate';
|
||||
|
||||
import lowestMatchedAncestor from '../../matchers/lowestMatchedAncestor';
|
||||
|
||||
function wrapSelectionInList(editor, type) {
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
Transforms.wrapNodes(editor, { type });
|
||||
const listItems = Editor.nodes(editor, lowestMatchedAncestor(editor, 'paragraph'));
|
||||
for (const listItem of listItems) {
|
||||
Transforms.wrapNodes(editor, { type: 'list-item' }, { at: listItem[1] });
|
||||
}
|
||||
});
|
||||
|
||||
Editor.normalize(editor);
|
||||
}
|
||||
|
||||
export default wrapSelectionInList;
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Editor, Element, Node, Transforms } from 'slate';
|
||||
|
||||
import keyDown from './events/keyDown';
|
||||
import moveListToListItem from './transforms/moveListToListItem';
|
||||
import toggleListType from './events/toggleListType';
|
||||
|
||||
function withLists(editor) {
|
||||
const { normalizeNode } = editor;
|
||||
if (editor.keyDownHandlers === undefined) {
|
||||
editor.keyDownHandlers = [];
|
||||
}
|
||||
editor.keyDownHandlers.push((event, editor) => keyDown(event, editor));
|
||||
|
||||
editor.toggleList = type => toggleListType(editor, type);
|
||||
|
||||
editor.isListItem = () => {
|
||||
const { selection } = editor;
|
||||
if (!selection) return false;
|
||||
|
||||
const [match] = Array.from(
|
||||
Editor.nodes(editor, {
|
||||
at: Editor.unhangRange(editor, selection),
|
||||
match: n =>
|
||||
!Editor.isEditor(n) &&
|
||||
Element.isElement(n) &&
|
||||
Editor.isBlock(editor, n) &&
|
||||
n.type !== 'paragraph' &&
|
||||
!`${n.type}`.startsWith('heading-'),
|
||||
mode: 'lowest',
|
||||
}),
|
||||
);
|
||||
|
||||
return !!match && match[0].type === 'list-item';
|
||||
};
|
||||
|
||||
editor.normalizeNode = entry => {
|
||||
normalizeNode(entry);
|
||||
const [node, path] = entry;
|
||||
|
||||
let previousType = null;
|
||||
if (Element.isElement(node) || Editor.isEditor(node)) {
|
||||
for (const [child, childPath] of Node.children(editor, path)) {
|
||||
if (`${child.type}`.endsWith('-list') && child.type === previousType) {
|
||||
Transforms.mergeNodes(editor, { at: childPath });
|
||||
break;
|
||||
}
|
||||
previousType = child.type;
|
||||
}
|
||||
}
|
||||
|
||||
if (Element.isElement(node) && `${node.type}`.endsWith('-list')) {
|
||||
const previousNode = Editor.previous(editor, { at: path });
|
||||
const [parentNode, parentNodePath] = Editor.parent(editor, path);
|
||||
|
||||
if (!previousNode && parentNode.type === 'list-item') {
|
||||
const previousListItem = Editor.previous(editor, { at: parentNodePath });
|
||||
moveListToListItem(editor, path, previousListItem);
|
||||
Transforms.removeNodes(editor, { at: parentNodePath });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
export default withLists;
|
||||
@@ -0,0 +1,6 @@
|
||||
import matchedAncestors from './matchedAncestors';
|
||||
|
||||
function lowestMatchedAncestor(editor, format) {
|
||||
return matchedAncestors(editor, format, 'lowest');
|
||||
}
|
||||
export default lowestMatchedAncestor;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Editor, Element } from 'slate';
|
||||
|
||||
function matchLink() {
|
||||
return {
|
||||
match: n => !Editor.isEditor(n) && Element.isElement(n) && n.type === 'link',
|
||||
};
|
||||
}
|
||||
|
||||
export default matchLink;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Editor, Element } from 'slate';
|
||||
|
||||
function matchedAncestors(editor, format, mode) {
|
||||
return {
|
||||
match: n =>
|
||||
(!Editor.isEditor(n) &&
|
||||
Element.isElement(n) &&
|
||||
Editor.isBlock(editor, n) &&
|
||||
(n.type === format ||
|
||||
(format === 'heading' && `${n.type}`.startsWith('heading-')) ||
|
||||
(format === 'paragraph' && `${n.type}`.startsWith('heading-')) ||
|
||||
(format === 'block' && !`${n.type}`.startsWith('heading-') && n.type !== 'paragraph') ||
|
||||
(format === 'list' && `${n.type}`.endsWith('-list')))) ||
|
||||
(format === 'non-default' && n.type !== 'paragraph'),
|
||||
mode,
|
||||
};
|
||||
}
|
||||
export default matchedAncestors;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Transforms } from 'slate';
|
||||
|
||||
import isCursorInEmptyParagraph from './locations/isCursorInEmptyParagraph';
|
||||
|
||||
function insertShortcode(editor, pluginConfig) {
|
||||
const defaultValues = pluginConfig.fields
|
||||
.toMap()
|
||||
.mapKeys((_, field) => field.get('name'))
|
||||
.filter(field => field.has('default'))
|
||||
.map(field => field.get('default'));
|
||||
|
||||
const nodeData = {
|
||||
type: 'shortcode',
|
||||
id: pluginConfig.id,
|
||||
data: {
|
||||
shortcode: pluginConfig.id,
|
||||
shortcodeNew: true,
|
||||
shortcodeData: defaultValues.toJS(),
|
||||
},
|
||||
children: [{ text: '' }],
|
||||
};
|
||||
|
||||
if (isCursorInEmptyParagraph(editor)) {
|
||||
Transforms.setNodes(editor, nodeData);
|
||||
return;
|
||||
}
|
||||
|
||||
Transforms.insertNodes(editor, nodeData);
|
||||
}
|
||||
|
||||
export default insertShortcode;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Editor, Element } from 'slate';
|
||||
|
||||
function isCursorInEmptyParagraph(editor) {
|
||||
const { selection } = editor;
|
||||
if (!selection) return false;
|
||||
|
||||
const [match] = Array.from(
|
||||
Editor.nodes(editor, {
|
||||
match: n => Element.isElement(n) && Editor.isBlock(editor, n) && n.type === 'paragraph',
|
||||
mode: 'lowest',
|
||||
}),
|
||||
);
|
||||
|
||||
return !!match && Editor.isEmpty(editor, match[0]);
|
||||
}
|
||||
|
||||
export default isCursorInEmptyParagraph;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Editor, Transforms } from 'slate';
|
||||
|
||||
import defaultEmptyBlock from '../blocks/defaultEmptyBlock';
|
||||
|
||||
function withShortcodes(editor) {
|
||||
const { isVoid, normalizeNode } = editor;
|
||||
|
||||
editor.isVoid = element => {
|
||||
return element.type === 'shortcode' ? true : isVoid(element);
|
||||
};
|
||||
|
||||
// Prevent empty editor after deleting shortcode theat was only child
|
||||
editor.normalizeNode = entry => {
|
||||
const [node] = entry;
|
||||
|
||||
if (Editor.isEditor(node) && node.children.length == 0) {
|
||||
Transforms.insertNodes(editor, defaultEmptyBlock());
|
||||
}
|
||||
|
||||
normalizeNode(entry);
|
||||
};
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
export default withShortcodes;
|
||||
@@ -0,0 +1,12 @@
|
||||
import castArray from 'lodash/castArray';
|
||||
import isArray from 'lodash/isArray';
|
||||
|
||||
export function assertType(nodes, type) {
|
||||
const nodesArray = castArray(nodes);
|
||||
const validate = isArray(type) ? node => type.includes(node.type) : node => type === node.type;
|
||||
const invalidNode = nodesArray.find(node => !validate(node));
|
||||
if (invalidNode) {
|
||||
throw Error(`Expected node of type "${type}", received "${invalidNode.type}".`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import React from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { colors, lengths } from 'decap-cms-ui-default';
|
||||
import { useSelected } from 'slate-react';
|
||||
|
||||
import VoidBlock from './components/VoidBlock';
|
||||
import Shortcode from './components/Shortcode';
|
||||
|
||||
const bottomMargin = '16px';
|
||||
|
||||
const headerStyles = `
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
`;
|
||||
|
||||
const StyledH1 = styled.h1`
|
||||
${headerStyles};
|
||||
font-size: 32px;
|
||||
margin-top: 16px;
|
||||
`;
|
||||
|
||||
const StyledH2 = styled.h2`
|
||||
${headerStyles};
|
||||
font-size: 24px;
|
||||
margin-top: 12px;
|
||||
`;
|
||||
|
||||
const StyledH3 = styled.h3`
|
||||
${headerStyles};
|
||||
font-size: 20px;
|
||||
`;
|
||||
|
||||
const StyledH4 = styled.h4`
|
||||
${headerStyles};
|
||||
font-size: 18px;
|
||||
margin-top: 8px;
|
||||
`;
|
||||
|
||||
const StyledH5 = styled.h5`
|
||||
${headerStyles};
|
||||
font-size: 16px;
|
||||
margin-top: 8px;
|
||||
`;
|
||||
|
||||
const StyledH6 = StyledH5.withComponent('h6');
|
||||
|
||||
const StyledP = styled.p`
|
||||
margin-bottom: ${bottomMargin};
|
||||
`;
|
||||
|
||||
const StyledBlockQuote = styled.blockquote`
|
||||
padding-left: 16px;
|
||||
border-left: 3px solid ${colors.background};
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: ${bottomMargin};
|
||||
`;
|
||||
|
||||
const StyledCode = styled.code`
|
||||
background-color: ${colors.background};
|
||||
border-radius: ${lengths.borderRadius};
|
||||
padding: 0 2px;
|
||||
font-size: 85%;
|
||||
`;
|
||||
|
||||
const StyledUl = styled.ul`
|
||||
margin-bottom: ${bottomMargin};
|
||||
padding-left: 30px;
|
||||
`;
|
||||
|
||||
const StyledOl = StyledUl.withComponent('ol');
|
||||
|
||||
const StyledLi = styled.li`
|
||||
& > p:first-of-type {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
& > p:last-of-type {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledA = styled.a`
|
||||
text-decoration: underline;
|
||||
font-size: inherit;
|
||||
`;
|
||||
|
||||
const StyledHr = styled.hr`
|
||||
border: 1px solid;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
const StyledTable = styled.table`
|
||||
border-collapse: collapse;
|
||||
`;
|
||||
|
||||
const StyledTd = styled.td`
|
||||
border: 2px solid black;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
/**
|
||||
* Slate uses React components to render each type of node that it receives.
|
||||
* This is the closest thing Slate has to a schema definition. The types are set
|
||||
* by us when we manually deserialize from Remark's MDAST to Slate's AST.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mark Components
|
||||
*/
|
||||
function Bold(props) {
|
||||
return <strong>{props.children}</strong>;
|
||||
}
|
||||
|
||||
function Italic(props) {
|
||||
return <em>{props.children}</em>;
|
||||
}
|
||||
|
||||
function Strikethrough(props) {
|
||||
return <s>{props.children}</s>;
|
||||
}
|
||||
|
||||
function Code(props) {
|
||||
return <StyledCode>{props.children}</StyledCode>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Node Components
|
||||
*/
|
||||
function Paragraph(props) {
|
||||
return <StyledP {...props.attributes}>{props.children}</StyledP>;
|
||||
}
|
||||
|
||||
function ListItem(props) {
|
||||
return <StyledLi {...props.attributes}>{props.children}</StyledLi>;
|
||||
}
|
||||
|
||||
function Quote(props) {
|
||||
return <StyledBlockQuote {...props.attributes}>{props.children}</StyledBlockQuote>;
|
||||
}
|
||||
|
||||
function HeadingOne(props) {
|
||||
return <StyledH1 {...props.attributes}>{props.children}</StyledH1>;
|
||||
}
|
||||
|
||||
function HeadingTwo(props) {
|
||||
return <StyledH2 {...props.attributes}>{props.children}</StyledH2>;
|
||||
}
|
||||
|
||||
function HeadingThree(props) {
|
||||
return <StyledH3 {...props.attributes}>{props.children}</StyledH3>;
|
||||
}
|
||||
|
||||
function HeadingFour(props) {
|
||||
return <StyledH4 {...props.attributes}>{props.children}</StyledH4>;
|
||||
}
|
||||
|
||||
function HeadingFive(props) {
|
||||
return <StyledH5 {...props.attributes}>{props.children}</StyledH5>;
|
||||
}
|
||||
|
||||
function HeadingSix(props) {
|
||||
return <StyledH6 {...props.attributes}>{props.children}</StyledH6>;
|
||||
}
|
||||
|
||||
function Table(props) {
|
||||
return (
|
||||
<StyledTable>
|
||||
<tbody {...props.attributes}>{props.children}</tbody>
|
||||
</StyledTable>
|
||||
);
|
||||
}
|
||||
|
||||
function TableRow(props) {
|
||||
return <tr {...props.attributes}>{props.children}</tr>;
|
||||
}
|
||||
|
||||
function TableCell(props) {
|
||||
return <StyledTd {...props.attributes}>{props.children}</StyledTd>;
|
||||
}
|
||||
|
||||
function ThematicBreak(props) {
|
||||
const isSelected = useSelected();
|
||||
return (
|
||||
<div {...props.attributes}>
|
||||
{props.children}
|
||||
<div contentEditable={false}>
|
||||
<StyledHr
|
||||
{...props.attributes}
|
||||
css={
|
||||
isSelected &&
|
||||
css`
|
||||
box-shadow: 0 0 0 2px ${colors.active};
|
||||
border-radius: 8px;
|
||||
color: ${colors.active};
|
||||
`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Break(props) {
|
||||
return (
|
||||
<>
|
||||
<br {...props.attributes} />
|
||||
{props.children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function BulletedList(props) {
|
||||
return <StyledUl {...props.attributes}>{props.children}</StyledUl>;
|
||||
}
|
||||
|
||||
function NumberedList(props) {
|
||||
return (
|
||||
<StyledOl {...props.attributes} start={1}>
|
||||
{props.children}
|
||||
</StyledOl>
|
||||
);
|
||||
}
|
||||
|
||||
function Link(props) {
|
||||
const url = props.element.url;
|
||||
const title = props.element.title || url;
|
||||
|
||||
return (
|
||||
<StyledA href={url} title={title} {...props.attributes}>
|
||||
{props.children}
|
||||
</StyledA>
|
||||
);
|
||||
}
|
||||
|
||||
function Image(props) {
|
||||
const { url, title, alt } = props.element.data;
|
||||
const isSelected = useSelected();
|
||||
return (
|
||||
<span {...props.attributes}>
|
||||
{props.children}
|
||||
<img
|
||||
src={url}
|
||||
title={title}
|
||||
alt={alt}
|
||||
{...props.attributes}
|
||||
css={
|
||||
isSelected &&
|
||||
css`
|
||||
box-shadow: 0 0 0 2px ${colors.active};
|
||||
`
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function Leaf({ attributes, children, leaf }) {
|
||||
if (leaf.bold) {
|
||||
children = <Bold>{children}</Bold>;
|
||||
}
|
||||
|
||||
if (leaf.italic) {
|
||||
children = <Italic>{children}</Italic>;
|
||||
}
|
||||
|
||||
if (leaf.delete) {
|
||||
children = <Strikethrough>{children}</Strikethrough>;
|
||||
}
|
||||
|
||||
if (leaf.code) {
|
||||
children = <Code>{children}</Code>;
|
||||
}
|
||||
|
||||
// if (leaf.break) {
|
||||
// children = <Break />;
|
||||
// }
|
||||
|
||||
return <span {...attributes}>{children}</span>;
|
||||
}
|
||||
|
||||
export function renderInline__DEPRECATED() {
|
||||
return props => {
|
||||
switch (props.node.type) {
|
||||
case 'link':
|
||||
return <Link {...props} />;
|
||||
case 'image':
|
||||
return <Image {...props} />;
|
||||
case 'break':
|
||||
return <Break {...props} />;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function Element(props) {
|
||||
const { children, element, classNameWrapper, codeBlockComponent } = props;
|
||||
const style = { textAlign: element.align };
|
||||
|
||||
switch (element.type) {
|
||||
case 'bulleted-list':
|
||||
return <BulletedList>{children}</BulletedList>;
|
||||
case 'quote':
|
||||
return <Quote>{children}</Quote>;
|
||||
case 'heading-one':
|
||||
return <HeadingOne>{children}</HeadingOne>;
|
||||
case 'heading-two':
|
||||
return <HeadingTwo>{children}</HeadingTwo>;
|
||||
case 'heading-three':
|
||||
return <HeadingThree>{children}</HeadingThree>;
|
||||
case 'heading-four':
|
||||
return <HeadingFour>{children}</HeadingFour>;
|
||||
case 'heading-five':
|
||||
return <HeadingFive>{children}</HeadingFive>;
|
||||
case 'heading-six':
|
||||
return <HeadingSix>{children}</HeadingSix>;
|
||||
case 'list-item':
|
||||
return <ListItem>{children}</ListItem>;
|
||||
case 'numbered-list':
|
||||
return <NumberedList>{children}</NumberedList>;
|
||||
case 'table':
|
||||
return <Table {...props} />;
|
||||
case 'table-row':
|
||||
return <TableRow {...props} />;
|
||||
case 'table-cell':
|
||||
return <TableCell {...props} />;
|
||||
case 'thematic-break':
|
||||
return (
|
||||
<VoidBlock {...props}>
|
||||
<ThematicBreak {...props} />
|
||||
</VoidBlock>
|
||||
);
|
||||
case 'link':
|
||||
return <Link {...props} />;
|
||||
case 'image':
|
||||
return <Image {...props} />;
|
||||
case 'break':
|
||||
return <Break {...props} />;
|
||||
case 'shortcode':
|
||||
if (element.id === 'code-block' && codeBlockComponent) {
|
||||
return (
|
||||
<VoidBlock {...props}>
|
||||
<Shortcode classNameWrapper={classNameWrapper} typeOverload="code-block" {...props} />
|
||||
</VoidBlock>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<VoidBlock {...props}>
|
||||
<Shortcode {...props}>{children}</Shortcode>
|
||||
</VoidBlock>
|
||||
);
|
||||
default:
|
||||
return <Paragraph style={style}>{children}</Paragraph>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { WidgetPreviewContainer } from 'decap-cms-ui-default';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
import { markdownToHtml } from './serializers';
|
||||
class MarkdownPreview extends React.Component {
|
||||
static propTypes = {
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
resolveWidget: PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// Manually validate PropTypes - React 19 breaking change
|
||||
PropTypes.checkPropTypes(MarkdownPreview.propTypes, this.props, 'prop', 'MarkdownPreview');
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value, getAsset, resolveWidget, field, getRemarkPlugins } = this.props;
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const html = markdownToHtml(value, { getAsset, resolveWidget }, getRemarkPlugins?.());
|
||||
const toRender = field?.get('sanitize_preview', false) ? DOMPurify.sanitize(html) : html;
|
||||
|
||||
return <WidgetPreviewContainer dangerouslySetInnerHTML={{ __html: toRender }} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default MarkdownPreview;
|
||||
@@ -0,0 +1,228 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import padStart from 'lodash/padStart';
|
||||
import { Map } from 'immutable';
|
||||
|
||||
import MarkdownPreview from '../MarkdownPreview';
|
||||
import { markdownToHtml } from '../serializers';
|
||||
|
||||
describe('Markdown Preview renderer', () => {
|
||||
describe('Markdown rendering', () => {
|
||||
describe('General', () => {
|
||||
it('should render markdown', async () => {
|
||||
const value = `
|
||||
# H1
|
||||
|
||||
Text with **bold** & _em_ elements
|
||||
|
||||
## H2
|
||||
|
||||
* ul item 1
|
||||
* ul item 2
|
||||
|
||||
### H3
|
||||
|
||||
1. ol item 1
|
||||
1. ol item 2
|
||||
1. ol item 3
|
||||
|
||||
#### H4
|
||||
|
||||
[link title](http://google.com)
|
||||
|
||||
##### H5
|
||||
|
||||

|
||||
|
||||
###### H6
|
||||
|
||||

|
||||
`;
|
||||
const html = await markdownToHtml(value);
|
||||
|
||||
const { container } = render(
|
||||
<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />,
|
||||
);
|
||||
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('H1');
|
||||
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('H2');
|
||||
expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent('H3');
|
||||
expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent('H4');
|
||||
expect(screen.getByRole('heading', { level: 5 })).toHaveTextContent('H5');
|
||||
expect(screen.getByRole('heading', { level: 6 })).toHaveTextContent('H6');
|
||||
expect(container).toHaveTextContent('Text with bold & em elements');
|
||||
expect(screen.getByRole('link', { name: 'link title' })).toHaveAttribute(
|
||||
'href',
|
||||
'http://google.com',
|
||||
);
|
||||
expect(screen.getAllByRole('img').length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Headings', () => {
|
||||
for (const heading of [...Array(6).keys()]) {
|
||||
it(`should render Heading ${heading + 1}`, async () => {
|
||||
const value = padStart(' Title', heading + 7, '#');
|
||||
const html = await markdownToHtml(value);
|
||||
|
||||
render(<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />);
|
||||
expect(screen.getByRole('heading', { level: heading + 1 })).toHaveTextContent('Title');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Lists', () => {
|
||||
it('should render lists', async () => {
|
||||
const value = `
|
||||
1. ol item 1
|
||||
1. ol item 2
|
||||
* Sublist 1
|
||||
* Sublist 2
|
||||
* Sublist 3
|
||||
1. Sub-Sublist 1
|
||||
1. Sub-Sublist 2
|
||||
1. Sub-Sublist 3
|
||||
1. ol item 3
|
||||
`;
|
||||
const html = await markdownToHtml(value);
|
||||
|
||||
const { container } = render(
|
||||
<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />,
|
||||
);
|
||||
// Check for ordered and unordered lists
|
||||
expect(container.querySelectorAll('ol').length).toBeGreaterThan(0);
|
||||
expect(container.querySelectorAll('ul').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('ol item 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Sublist 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Sub-Sublist 1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Links', () => {
|
||||
it('should render links', async () => {
|
||||
const value = `
|
||||
I get 10 times more traffic from [Google] than from [Yahoo] or [MSN].
|
||||
|
||||
[Google]: http://google.com/ "Google"
|
||||
[Yahoo]: http://search.yahoo.com/ "Yahoo Search"
|
||||
[MSN]: http://search.msn.com/ "MSN Search"
|
||||
`;
|
||||
const html = await markdownToHtml(value);
|
||||
|
||||
render(<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />);
|
||||
expect(screen.getByRole('link', { name: 'Google' })).toHaveAttribute(
|
||||
'href',
|
||||
'http://google.com/',
|
||||
);
|
||||
expect(screen.getByRole('link', { name: 'Yahoo' })).toHaveAttribute(
|
||||
'href',
|
||||
'http://search.yahoo.com/',
|
||||
);
|
||||
expect(screen.getByRole('link', { name: 'MSN' })).toHaveAttribute(
|
||||
'href',
|
||||
'http://search.msn.com/',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Code', () => {
|
||||
it('should render code', async () => {
|
||||
const value = 'Use the `printf()` function.';
|
||||
const html = await markdownToHtml(value);
|
||||
|
||||
const { container } = render(
|
||||
<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />,
|
||||
);
|
||||
expect(container.querySelector('code')).toHaveTextContent('printf()');
|
||||
});
|
||||
|
||||
it('should render code 2', async () => {
|
||||
const value = '``There is a literal backtick (`) here.``';
|
||||
const html = await markdownToHtml(value);
|
||||
|
||||
const { container } = render(
|
||||
<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />,
|
||||
);
|
||||
expect(container.querySelector('code')).toHaveTextContent(
|
||||
'There is a literal backtick (`) here.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTML', () => {
|
||||
it('should render HTML as is when using Markdown', async () => {
|
||||
const value = `
|
||||
# Title
|
||||
|
||||
<form action="test">
|
||||
<label for="input">
|
||||
<input type="checkbox" checked="checked" id="input"/> My label
|
||||
</label>
|
||||
<dl class="test-class another-class" style="width: 100%">
|
||||
<dt data-attr="test">Test HTML content</dt>
|
||||
<dt>Testing HTML in Markdown</dt>
|
||||
</dl>
|
||||
</form>
|
||||
|
||||
<h1 style="display: block; border: 10px solid #f00; width: 100%">Test</h1>
|
||||
`;
|
||||
const html = await markdownToHtml(value);
|
||||
|
||||
const { container } = render(
|
||||
<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />,
|
||||
);
|
||||
expect(container.querySelector('form')).toBeInTheDocument();
|
||||
expect(container.querySelector('dl')).toBeInTheDocument();
|
||||
expect(container.querySelector('h1[style]')).toHaveTextContent('Test');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTML rendering', () => {
|
||||
it('should render HTML', async () => {
|
||||
const value = '<p>Paragraph with <em>inline</em> element</p>';
|
||||
const html = await markdownToHtml(value);
|
||||
|
||||
const { container } = render(
|
||||
<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />,
|
||||
);
|
||||
expect(container.querySelector('p')).toHaveTextContent('Paragraph with inline element');
|
||||
expect(container.querySelector('em')).toHaveTextContent('inline');
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTML sanitization', () => {
|
||||
it('should sanitize HTML', async () => {
|
||||
const value = `<img src="foobar.png" onerror="alert('hello')">`;
|
||||
const field = Map({ sanitize_preview: true });
|
||||
|
||||
const { container } = render(
|
||||
<MarkdownPreview
|
||||
value={value}
|
||||
getAsset={jest.fn()}
|
||||
resolveWidget={jest.fn()}
|
||||
field={field}
|
||||
/>,
|
||||
);
|
||||
const img = container.querySelector('img');
|
||||
expect(img).toHaveAttribute('src', 'foobar.png');
|
||||
expect(img).not.toHaveAttribute('onerror');
|
||||
});
|
||||
|
||||
it('should not sanitize HTML', async () => {
|
||||
const value = `<img src="foobar.png" onerror="alert('hello')">`;
|
||||
const field = Map({ sanitize_preview: false });
|
||||
|
||||
const { container } = render(
|
||||
<MarkdownPreview
|
||||
value={value}
|
||||
getAsset={jest.fn()}
|
||||
resolveWidget={jest.fn()}
|
||||
field={field}
|
||||
/>,
|
||||
);
|
||||
const img = container.querySelector('img');
|
||||
expect(img).toHaveAttribute('src', 'foobar.png');
|
||||
expect(img).toHaveAttribute('onerror', "alert('hello')");
|
||||
});
|
||||
});
|
||||
});
|
||||
16
source/admin/packages/decap-cms-widget-markdown/src/index.js
Normal file
16
source/admin/packages/decap-cms-widget-markdown/src/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import controlComponent from './MarkdownControl';
|
||||
import previewComponent from './MarkdownPreview';
|
||||
import schema from './schema';
|
||||
|
||||
function Widget(opts = {}) {
|
||||
return {
|
||||
name: 'markdown',
|
||||
controlComponent,
|
||||
previewComponent,
|
||||
schema,
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
|
||||
export const DecapCmsWidgetMarkdown = { Widget, controlComponent, previewComponent };
|
||||
export default DecapCmsWidgetMarkdown;
|
||||
@@ -0,0 +1,137 @@
|
||||
import last from 'lodash/last';
|
||||
|
||||
/**
|
||||
* Joins an array of regular expressions into a single expression, without
|
||||
* altering the received expressions.
|
||||
*/
|
||||
export function joinPatternSegments(patterns) {
|
||||
return patterns.map(p => p.source).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines an array of regular expressions into a single expression, wrapping
|
||||
* each in a non-capturing group and interposing alternation characters (|) so
|
||||
* that each expression is executed separately.
|
||||
*/
|
||||
export function combinePatterns(patterns) {
|
||||
return patterns.map(p => `(?:${p.source})`).join('|');
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify substrings within a string if they match a (global) pattern. Can be
|
||||
* inverted to only modify non-matches.
|
||||
*
|
||||
* params:
|
||||
* matchPattern - regexp - a regular expression to check for matches
|
||||
* replaceFn - function - a replacement function that receives a matched
|
||||
* substring and returns a replacement substring
|
||||
* text - string - the string to process
|
||||
* invertMatchPattern - boolean - if true, non-matching substrings are modified
|
||||
* instead of matching substrings
|
||||
*/
|
||||
export function replaceWhen(matchPattern, replaceFn, text, invertMatchPattern) {
|
||||
/**
|
||||
* Splits the string into an array of objects with the following shape:
|
||||
*
|
||||
* {
|
||||
* index: number - the index of the substring within the string
|
||||
* text: string - the substring
|
||||
* match: boolean - true if the substring matched `matchPattern`
|
||||
* }
|
||||
*
|
||||
* Loops through matches via recursion (`RegExp.exec` tracks the loop
|
||||
* internally).
|
||||
*/
|
||||
function split(exp, text, acc) {
|
||||
/**
|
||||
* Get the next match starting from the end of the last match or start of
|
||||
* string.
|
||||
*/
|
||||
const match = exp.exec(text);
|
||||
const lastEntry = last(acc);
|
||||
|
||||
/**
|
||||
* `match` will be null if there are no matches.
|
||||
*/
|
||||
if (!match) return acc;
|
||||
|
||||
/**
|
||||
* If the match is at the beginning of the input string, normalize to a data
|
||||
* object with the `match` flag set to `true`, and add to the accumulator.
|
||||
*/
|
||||
if (match.index === 0) {
|
||||
addSubstring(acc, 0, match[0], true);
|
||||
} else if (!lastEntry) {
|
||||
/**
|
||||
* If there are no entries in the accumulator, convert the substring before
|
||||
* the match to a data object (without the `match` flag set to true) and
|
||||
* push to the accumulator, followed by a data object for the matching
|
||||
* substring.
|
||||
*/
|
||||
addSubstring(acc, 0, match.input.slice(0, match.index));
|
||||
addSubstring(acc, match.index, match[0], true);
|
||||
} else if (match.index === lastEntry.index + lastEntry.text.length) {
|
||||
/**
|
||||
* If the last entry in the accumulator immediately preceded the current
|
||||
* matched substring in the original string, just add the data object for
|
||||
* the matching substring to the accumulator.
|
||||
*/
|
||||
addSubstring(acc, match.index, match[0], true);
|
||||
} else {
|
||||
/**
|
||||
* Convert the substring before the match to a data object (without the
|
||||
* `match` flag set to true), followed by a data object for the matching
|
||||
* substring.
|
||||
*/
|
||||
const nextIndex = lastEntry.index + lastEntry.text.length;
|
||||
const nextText = match.input.slice(nextIndex, match.index);
|
||||
addSubstring(acc, nextIndex, nextText);
|
||||
addSubstring(acc, match.index, match[0], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue executing the expression.
|
||||
*/
|
||||
return split(exp, text, acc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for converting substrings to data objects and adding to an output
|
||||
* array.
|
||||
*/
|
||||
function addSubstring(arr, index, text, match = false) {
|
||||
arr.push({ index, text, match });
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the input string to an array of data objects, each representing a
|
||||
* matching or non-matching string.
|
||||
*/
|
||||
const acc = split(matchPattern, text, []);
|
||||
|
||||
/**
|
||||
* Process the trailing substring after the final match, if one exists.
|
||||
*/
|
||||
const lastEntry = last(acc);
|
||||
if (!lastEntry) return replaceFn(text);
|
||||
|
||||
const nextIndex = lastEntry.index + lastEntry.text.length;
|
||||
if (text.length > nextIndex) {
|
||||
acc.push({ index: nextIndex, text: text.slice(nextIndex) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Map the data objects in the accumulator to their string values, modifying
|
||||
* matched strings with the replacement function. Modifies non-matches if
|
||||
* `invertMatchPattern` is truthy.
|
||||
*/
|
||||
const replacedText = acc.map(entry => {
|
||||
const isMatch = invertMatchPattern ? !entry.match : entry.match;
|
||||
return isMatch ? replaceFn(entry.text) : entry.text;
|
||||
});
|
||||
|
||||
/**
|
||||
* Return the joined string.
|
||||
*/
|
||||
return replacedText.join('');
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
export default {
|
||||
properties: {
|
||||
minimal: { type: 'boolean' },
|
||||
buttons: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: [
|
||||
'bold',
|
||||
'italic',
|
||||
'code',
|
||||
'link',
|
||||
'heading-one',
|
||||
'heading-two',
|
||||
'heading-three',
|
||||
'heading-four',
|
||||
'heading-five',
|
||||
'heading-six',
|
||||
'quote',
|
||||
'bulleted-list',
|
||||
'numbered-list',
|
||||
],
|
||||
},
|
||||
},
|
||||
editor_components: { type: 'array', items: { type: 'string' } },
|
||||
modes: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: ['raw', 'rich_text'],
|
||||
},
|
||||
minItems: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,625 @@
|
||||
{
|
||||
"\tfoo\tbaz\t\tbim\n": "NOT_TO_EQUAL",
|
||||
" \tfoo\tbaz\t\tbim\n": "NOT_TO_EQUAL",
|
||||
" a\ta\n ὐ\ta\n": "NOT_TO_EQUAL",
|
||||
" - foo\n\n\tbar\n": "NOT_TO_EQUAL",
|
||||
"- foo\n\n\t\tbar\n": "NOT_TO_EQUAL",
|
||||
">\t\tfoo\n": "NOT_TO_EQUAL",
|
||||
"-\t\tfoo\n": "NOT_TO_EQUAL",
|
||||
" foo\n\tbar\n": "TO_EQUAL",
|
||||
" - foo\n - bar\n\t - baz\n": "NOT_TO_EQUAL",
|
||||
"#\tFoo\n": "TO_EQUAL",
|
||||
"*\t*\t*\t\n": "TO_ERROR",
|
||||
"- `one\n- two`\n": "TO_EQUAL",
|
||||
"***\n---\n___\n": "NOT_TO_EQUAL",
|
||||
"+++\n": "TO_EQUAL",
|
||||
"===\n": "TO_EQUAL",
|
||||
"--\n**\n__\n": "TO_EQUAL",
|
||||
" ***\n ***\n ***\n": "NOT_TO_EQUAL",
|
||||
" ***\n": "TO_EQUAL",
|
||||
"Foo\n ***\n": "TO_EQUAL",
|
||||
"_____________________________________\n": "NOT_TO_EQUAL",
|
||||
" - - -\n": "NOT_TO_EQUAL",
|
||||
" ** * ** * ** * **\n": "NOT_TO_EQUAL",
|
||||
"- - - -\n": "NOT_TO_EQUAL",
|
||||
"- - - - \n": "NOT_TO_EQUAL",
|
||||
"_ _ _ _ a\n\na------\n\n---a---\n": "TO_EQUAL",
|
||||
" *-*\n": "NOT_TO_EQUAL",
|
||||
"- foo\n***\n- bar\n": "NOT_TO_EQUAL",
|
||||
"Foo\n***\nbar\n": "NOT_TO_EQUAL",
|
||||
"Foo\n---\nbar\n": "TO_EQUAL",
|
||||
"* Foo\n* * *\n* Bar\n": "NOT_TO_EQUAL",
|
||||
"- Foo\n- * * *\n": "NOT_TO_EQUAL",
|
||||
"# foo\n## foo\n### foo\n#### foo\n##### foo\n###### foo\n": "TO_EQUAL",
|
||||
"####### foo\n": "TO_EQUAL",
|
||||
"#5 bolt\n\n#hashtag\n": "TO_EQUAL",
|
||||
"\\## foo\n": "TO_EQUAL",
|
||||
"# foo *bar* \\*baz\\*\n": "NOT_TO_EQUAL",
|
||||
"# foo \n": "TO_EQUAL",
|
||||
" ### foo\n ## foo\n # foo\n": "TO_EQUAL",
|
||||
" # foo\n": "TO_EQUAL",
|
||||
"foo\n # bar\n": "TO_EQUAL",
|
||||
"## foo ##\n ### bar ###\n": "TO_EQUAL",
|
||||
"# foo ##################################\n##### foo ##\n": "TO_EQUAL",
|
||||
"### foo ### \n": "TO_EQUAL",
|
||||
"### foo ### b\n": "TO_EQUAL",
|
||||
"# foo#\n": "NOT_TO_EQUAL",
|
||||
"### foo \\###\n## foo #\\##\n# foo \\#\n": "NOT_TO_EQUAL",
|
||||
"****\n## foo\n****\n": "NOT_TO_EQUAL",
|
||||
"Foo bar\n# baz\nBar foo\n": "TO_EQUAL",
|
||||
"## \n#\n### ###\n": "TO_ERROR",
|
||||
"Foo *bar*\n=========\n\nFoo *bar*\n---------\n": "TO_EQUAL",
|
||||
"Foo *bar\nbaz*\n====\n": "NOT_TO_EQUAL",
|
||||
"Foo\n-------------------------\n\nFoo\n=\n": "TO_EQUAL",
|
||||
" Foo\n---\n\n Foo\n-----\n\n Foo\n ===\n": "NOT_TO_EQUAL",
|
||||
" Foo\n ---\n\n Foo\n---\n": "NOT_TO_EQUAL",
|
||||
"Foo\n ---- \n": "NOT_TO_EQUAL",
|
||||
"Foo\n ---\n": "TO_EQUAL",
|
||||
"Foo\n= =\n\nFoo\n--- -\n": "NOT_TO_EQUAL",
|
||||
"Foo \n-----\n": "TO_EQUAL",
|
||||
"Foo\\\n----\n": "TO_EQUAL",
|
||||
"`Foo\n----\n`\n\n<a title=\"a lot\n---\nof dashes\"/>\n": "NOT_TO_EQUAL",
|
||||
"> Foo\n---\n": "NOT_TO_EQUAL",
|
||||
"> foo\nbar\n===\n": "NOT_TO_EQUAL",
|
||||
"- Foo\n---\n": "NOT_TO_EQUAL",
|
||||
"Foo\nBar\n---\n": "NOT_TO_EQUAL",
|
||||
"---\nFoo\n---\nBar\n---\nBaz\n": "NOT_TO_EQUAL",
|
||||
"\n====\n": "TO_EQUAL",
|
||||
"---\n---\n": "TO_ERROR",
|
||||
"- foo\n-----\n": "NOT_TO_EQUAL",
|
||||
" foo\n---\n": "NOT_TO_EQUAL",
|
||||
"> foo\n-----\n": "NOT_TO_EQUAL",
|
||||
"\\> foo\n------\n": "NOT_TO_EQUAL",
|
||||
"Foo\n\nbar\n---\nbaz\n": "TO_EQUAL",
|
||||
"Foo\nbar\n\n---\n\nbaz\n": "NOT_TO_EQUAL",
|
||||
"Foo\nbar\n* * *\nbaz\n": "NOT_TO_EQUAL",
|
||||
"Foo\nbar\n\\---\nbaz\n": "NOT_TO_EQUAL",
|
||||
" a simple\n indented code block\n": "TO_EQUAL",
|
||||
" - foo\n\n bar\n": "NOT_TO_EQUAL",
|
||||
"1. foo\n\n - bar\n": "TO_EQUAL",
|
||||
" <a/>\n *hi*\n\n - one\n": "NOT_TO_EQUAL",
|
||||
" chunk1\n\n chunk2\n \n \n \n chunk3\n": "TO_EQUAL",
|
||||
" chunk1\n \n chunk2\n": "TO_EQUAL",
|
||||
"Foo\n bar\n\n": "TO_EQUAL",
|
||||
" foo\nbar\n": "TO_EQUAL",
|
||||
"# Heading\n foo\nHeading\n------\n foo\n----\n": "NOT_TO_EQUAL",
|
||||
" foo\n bar\n": "TO_EQUAL",
|
||||
"\n \n foo\n \n\n": "TO_EQUAL",
|
||||
" foo \n": "TO_EQUAL",
|
||||
"```\n<\n >\n```\n": "NOT_TO_EQUAL",
|
||||
"~~~\n<\n >\n~~~\n": "NOT_TO_EQUAL",
|
||||
"``\nfoo\n``\n": "TO_EQUAL",
|
||||
"```\naaa\n~~~\n```\n": "NOT_TO_EQUAL",
|
||||
"~~~\naaa\n```\n~~~\n": "NOT_TO_EQUAL",
|
||||
"````\naaa\n```\n``````\n": "NOT_TO_EQUAL",
|
||||
"~~~~\naaa\n~~~\n~~~~\n": "NOT_TO_EQUAL",
|
||||
"```\n": "TO_EQUAL",
|
||||
"`````\n\n```\naaa\n": "NOT_TO_EQUAL",
|
||||
"> ```\n> aaa\n\nbbb\n": "TO_EQUAL",
|
||||
"```\n\n \n```\n": "NOT_TO_EQUAL",
|
||||
"```\n```\n": "TO_EQUAL",
|
||||
" ```\n aaa\naaa\n```\n": "TO_EQUAL",
|
||||
" ```\naaa\n aaa\naaa\n ```\n": "TO_EQUAL",
|
||||
" ```\n aaa\n aaa\n aaa\n ```\n": "TO_EQUAL",
|
||||
" ```\n aaa\n ```\n": "NOT_TO_EQUAL",
|
||||
"```\naaa\n ```\n": "TO_EQUAL",
|
||||
" ```\naaa\n ```\n": "TO_EQUAL",
|
||||
"```\naaa\n ```\n": "NOT_TO_EQUAL",
|
||||
"``` ```\naaa\n": "TO_EQUAL",
|
||||
"~~~~~~\naaa\n~~~ ~~\n": "NOT_TO_EQUAL",
|
||||
"foo\n```\nbar\n```\nbaz\n": "TO_EQUAL",
|
||||
"foo\n---\n~~~\nbar\n~~~\n# baz\n": "TO_EQUAL",
|
||||
"```ruby\ndef foo(x)\n return 3\nend\n```\n": "TO_EQUAL",
|
||||
"~~~~ ruby startline=3 $%@#$\ndef foo(x)\n return 3\nend\n~~~~~~~\n": "TO_EQUAL",
|
||||
"````;\n````\n": "TO_EQUAL",
|
||||
"``` aa ```\nfoo\n": "TO_EQUAL",
|
||||
"```\n``` aaa\n```\n": "NOT_TO_EQUAL",
|
||||
"<table><tr><td>\n<pre>\n**Hello**,\n\n_world_.\n</pre>\n</td></tr></table>\n": "NOT_TO_EQUAL",
|
||||
"<table>\n <tr>\n <td>\n hi\n </td>\n </tr>\n</table>\n\nokay.\n": "TO_EQUAL",
|
||||
" <div>\n *hello*\n <foo><a>\n": "NOT_TO_EQUAL",
|
||||
"</div>\n*foo*\n": "NOT_TO_EQUAL",
|
||||
"<DIV CLASS=\"foo\">\n\n*Markdown*\n\n</DIV>\n": "TO_EQUAL",
|
||||
"<div id=\"foo\"\n class=\"bar\">\n</div>\n": "TO_EQUAL",
|
||||
"<div id=\"foo\" class=\"bar\n baz\">\n</div>\n": "TO_EQUAL",
|
||||
"<div>\n*foo*\n\n*bar*\n": "NOT_TO_EQUAL",
|
||||
"<div id=\"foo\"\n*hi*\n": "NOT_TO_EQUAL",
|
||||
"<div class\nfoo\n": "TO_EQUAL",
|
||||
"<div *???-&&&-<---\n*foo*\n": "NOT_TO_EQUAL",
|
||||
"<div><a href=\"bar\">*foo*</a></div>\n": "NOT_TO_EQUAL",
|
||||
"<table><tr><td>\nfoo\n</td></tr></table>\n": "TO_EQUAL",
|
||||
"<div></div>\n``` c\nint x = 33;\n```\n": "NOT_TO_EQUAL",
|
||||
"<a href=\"foo\">\n*bar*\n</a>\n": "NOT_TO_EQUAL",
|
||||
"<Warning>\n*bar*\n</Warning>\n": "NOT_TO_EQUAL",
|
||||
"<i class=\"foo\">\n*bar*\n</i>\n": "NOT_TO_EQUAL",
|
||||
"</ins>\n*bar*\n": "NOT_TO_EQUAL",
|
||||
"<del>\n*foo*\n</del>\n": "NOT_TO_EQUAL",
|
||||
"<del>\n\n*foo*\n\n</del>\n": "TO_EQUAL",
|
||||
"<del>*foo*</del>\n": "TO_EQUAL",
|
||||
"<pre language=\"haskell\"><code>\nimport Text.HTML.TagSoup\n\nmain :: IO ()\nmain = print $ parseTags tags\n</code></pre>\nokay\n": "TO_EQUAL",
|
||||
"<script type=\"text/javascript\">\n// JavaScript example\n\ndocument.getElementById(\"demo\").innerHTML = \"Hello JavaScript!\";\n</script>\nokay\n": "TO_EQUAL",
|
||||
"<style\n type=\"text/css\">\nh1 {color:red;}\n\np {color:blue;}\n</style>\nokay\n": "TO_EQUAL",
|
||||
"<style\n type=\"text/css\">\n\nfoo\n": "TO_EQUAL",
|
||||
"> <div>\n> foo\n\nbar\n": "TO_EQUAL",
|
||||
"- <div>\n- foo\n": "TO_EQUAL",
|
||||
"<style>p{color:red;}</style>\n*foo*\n": "TO_EQUAL",
|
||||
"<!-- foo -->*bar*\n*baz*\n": "NOT_TO_EQUAL",
|
||||
"<script>\nfoo\n</script>1. *bar*\n": "NOT_TO_EQUAL",
|
||||
"<!-- Foo\n\nbar\n baz -->\nokay\n": "TO_EQUAL",
|
||||
"<?php\n\n echo '>';\n\n?>\nokay\n": "TO_EQUAL",
|
||||
"<!DOCTYPE html>\n": "TO_EQUAL",
|
||||
"<![CDATA[\nfunction matchwo(a,b)\n{\n if (a < b && a < 0) then {\n return 1;\n\n } else {\n\n return 0;\n }\n}\n]]>\nokay\n": "NOT_TO_EQUAL",
|
||||
" <!-- foo -->\n\n <!-- foo -->\n": "NOT_TO_EQUAL",
|
||||
" <div>\n\n <div>\n": "NOT_TO_EQUAL",
|
||||
"Foo\n<div>\nbar\n</div>\n": "TO_EQUAL",
|
||||
"<div>\nbar\n</div>\n*foo*\n": "NOT_TO_EQUAL",
|
||||
"Foo\n<a href=\"bar\">\nbaz\n": "TO_EQUAL",
|
||||
"<div>\n\n*Emphasized* text.\n\n</div>\n": "TO_EQUAL",
|
||||
"<div>\n*Emphasized* text.\n</div>\n": "NOT_TO_EQUAL",
|
||||
"<table>\n\n<tr>\n\n<td>\nHi\n</td>\n\n</tr>\n\n</table>\n": "TO_EQUAL",
|
||||
"<table>\n\n <tr>\n\n <td>\n Hi\n </td>\n\n </tr>\n\n</table>\n": "NOT_TO_EQUAL",
|
||||
"[foo]: /url \"title\"\n\n[foo]\n": "TO_EQUAL",
|
||||
" [foo]: \n /url \n 'the title' \n\n[foo]\n": "TO_EQUAL",
|
||||
"[Foo*bar\\]]:my_(url) 'title (with parens)'\n\n[Foo*bar\\]]\n": "NOT_TO_EQUAL",
|
||||
"[Foo bar]:\n<my%20url>\n'title'\n\n[Foo bar]\n": "TO_EQUAL",
|
||||
"[foo]: /url '\ntitle\nline1\nline2\n'\n\n[foo]\n": "NOT_TO_EQUAL",
|
||||
"[foo]: /url 'title\n\nwith blank line'\n\n[foo]\n": "TO_EQUAL",
|
||||
"[foo]:\n/url\n\n[foo]\n": "TO_EQUAL",
|
||||
"[foo]:\n\n[foo]\n": "TO_EQUAL",
|
||||
"[foo]: /url\\bar\\*baz \"foo\\\"bar\\baz\"\n\n[foo]\n": "NOT_TO_EQUAL",
|
||||
"[foo]\n\n[foo]: url\n": "TO_EQUAL",
|
||||
"[foo]\n\n[foo]: first\n[foo]: second\n": "NOT_TO_EQUAL",
|
||||
"[FOO]: /url\n\n[Foo]\n": "TO_EQUAL",
|
||||
"[ΑΓΩ]: /φου\n\n[αγω]\n": "TO_EQUAL",
|
||||
"[foo]: /url\n": "TO_ERROR",
|
||||
"[\nfoo\n]: /url\nbar\n": "TO_EQUAL",
|
||||
"[foo]: /url \"title\" ok\n": "NOT_TO_EQUAL",
|
||||
"[foo]: /url\n\"title\" ok\n": "NOT_TO_EQUAL",
|
||||
" [foo]: /url \"title\"\n\n[foo]\n": "NOT_TO_EQUAL",
|
||||
"```\n[foo]: /url\n```\n\n[foo]\n": "TO_EQUAL",
|
||||
"Foo\n[bar]: /baz\n\n[bar]\n": "TO_EQUAL",
|
||||
"# [Foo]\n[foo]: /url\n> bar\n": "TO_EQUAL",
|
||||
"[foo]: /foo-url \"foo\"\n[bar]: /bar-url\n \"bar\"\n[baz]: /baz-url\n\n[foo],\n[bar],\n[baz]\n": "TO_EQUAL",
|
||||
"[foo]\n\n> [foo]: /url\n": "NOT_TO_EQUAL",
|
||||
"aaa\n\nbbb\n": "TO_EQUAL",
|
||||
"aaa\nbbb\n\nccc\nddd\n": "TO_EQUAL",
|
||||
"aaa\n\n\nbbb\n": "TO_EQUAL",
|
||||
" aaa\n bbb\n": "NOT_TO_EQUAL",
|
||||
"aaa\n bbb\n ccc\n": "TO_EQUAL",
|
||||
" aaa\nbbb\n": "NOT_TO_EQUAL",
|
||||
" aaa\nbbb\n": "TO_EQUAL",
|
||||
"aaa \nbbb \n": "NOT_TO_EQUAL",
|
||||
" \n\naaa\n \n\n# aaa\n\n \n": "TO_EQUAL",
|
||||
"> # Foo\n> bar\n> baz\n": "TO_EQUAL",
|
||||
"># Foo\n>bar\n> baz\n": "TO_EQUAL",
|
||||
" > # Foo\n > bar\n > baz\n": "TO_EQUAL",
|
||||
" > # Foo\n > bar\n > baz\n": "NOT_TO_EQUAL",
|
||||
"> # Foo\n> bar\nbaz\n": "TO_EQUAL",
|
||||
"> bar\nbaz\n> foo\n": "TO_EQUAL",
|
||||
"> foo\n---\n": "NOT_TO_EQUAL",
|
||||
"> - foo\n- bar\n": "TO_EQUAL",
|
||||
"> foo\n bar\n": "TO_EQUAL",
|
||||
"> ```\nfoo\n```\n": "NOT_TO_EQUAL",
|
||||
"> foo\n - bar\n": "NOT_TO_EQUAL",
|
||||
">\n": "TO_ERROR",
|
||||
">\n> \n> \n": "TO_ERROR",
|
||||
">\n> foo\n> \n": "TO_EQUAL",
|
||||
"> foo\n\n> bar\n": "NOT_TO_EQUAL",
|
||||
"> foo\n> bar\n": "TO_EQUAL",
|
||||
"> foo\n>\n> bar\n": "TO_EQUAL",
|
||||
"foo\n> bar\n": "TO_EQUAL",
|
||||
"> aaa\n***\n> bbb\n": "NOT_TO_EQUAL",
|
||||
"> bar\nbaz\n": "TO_EQUAL",
|
||||
"> bar\n\nbaz\n": "TO_EQUAL",
|
||||
"> bar\n>\nbaz\n": "NOT_TO_EQUAL",
|
||||
"> > > foo\nbar\n": "TO_EQUAL",
|
||||
">>> foo\n> bar\n>>baz\n": "TO_EQUAL",
|
||||
"> code\n\n> not code\n": "NOT_TO_EQUAL",
|
||||
"A paragraph\nwith two lines.\n\n indented code\n\n> A block quote.\n": "TO_EQUAL",
|
||||
"1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL",
|
||||
"- one\n\n two\n": "NOT_TO_EQUAL",
|
||||
"- one\n\n two\n": "NOT_TO_EQUAL",
|
||||
" - one\n\n two\n": "NOT_TO_EQUAL",
|
||||
" - one\n\n two\n": "NOT_TO_EQUAL",
|
||||
" > > 1. one\n>>\n>> two\n": "NOT_TO_EQUAL",
|
||||
">>- one\n>>\n > > two\n": "TO_EQUAL",
|
||||
"-one\n\n2.two\n": "TO_EQUAL",
|
||||
"- foo\n\n\n bar\n": "TO_EQUAL",
|
||||
"1. foo\n\n ```\n bar\n ```\n\n baz\n\n > bam\n": "TO_EQUAL",
|
||||
"- Foo\n\n bar\n\n\n baz\n": "NOT_TO_EQUAL",
|
||||
"123456789. ok\n": "TO_EQUAL",
|
||||
"1234567890. not ok\n": "NOT_TO_EQUAL",
|
||||
"0. ok\n": "NOT_TO_EQUAL",
|
||||
"003. ok\n": "TO_EQUAL",
|
||||
"-1. not ok\n": "TO_EQUAL",
|
||||
"- foo\n\n bar\n": "TO_EQUAL",
|
||||
" 10. foo\n\n bar\n": "TO_EQUAL",
|
||||
" indented code\n\nparagraph\n\n more code\n": "TO_EQUAL",
|
||||
"1. indented code\n\n paragraph\n\n more code\n": "TO_EQUAL",
|
||||
"1. indented code\n\n paragraph\n\n more code\n": "TO_EQUAL",
|
||||
" foo\n\nbar\n": "NOT_TO_EQUAL",
|
||||
"- foo\n\n bar\n": "NOT_TO_EQUAL",
|
||||
"- foo\n\n bar\n": "NOT_TO_EQUAL",
|
||||
"-\n foo\n-\n ```\n bar\n ```\n-\n baz\n": "NOT_TO_EQUAL",
|
||||
"- \n foo\n": "TO_ERROR",
|
||||
"-\n\n foo\n": "NOT_TO_EQUAL",
|
||||
"- foo\n-\n- bar\n": "TO_ERROR",
|
||||
"- foo\n- \n- bar\n": "TO_ERROR",
|
||||
"1. foo\n2.\n3. bar\n": "TO_ERROR",
|
||||
"*\n": "NOT_TO_EQUAL",
|
||||
"foo\n*\n\nfoo\n1.\n": "TO_EQUAL",
|
||||
" 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL",
|
||||
" 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL",
|
||||
" 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "TO_EQUAL",
|
||||
" 1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n": "NOT_TO_EQUAL",
|
||||
" 1. A paragraph\nwith two lines.\n\n indented code\n\n > A block quote.\n": "NOT_TO_EQUAL",
|
||||
" 1. A paragraph\n with two lines.\n": "TO_EQUAL",
|
||||
"> 1. > Blockquote\ncontinued here.\n": "TO_EQUAL",
|
||||
"> 1. > Blockquote\n> continued here.\n": "TO_EQUAL",
|
||||
"- foo\n - bar\n - baz\n - boo\n": "NOT_TO_EQUAL",
|
||||
"- foo\n - bar\n - baz\n - boo\n": "TO_EQUAL",
|
||||
"10) foo\n - bar\n": "NOT_TO_EQUAL",
|
||||
"10) foo\n - bar\n": "TO_EQUAL",
|
||||
"- - foo\n": "TO_EQUAL",
|
||||
"1. - 2. foo\n": "TO_EQUAL",
|
||||
"- # Foo\n- Bar\n ---\n baz\n": "NOT_TO_EQUAL",
|
||||
"- foo\n- bar\n+ baz\n": "TO_EQUAL",
|
||||
"1. foo\n2. bar\n3) baz\n": "TO_EQUAL",
|
||||
"Foo\n- bar\n- baz\n": "TO_EQUAL",
|
||||
"The number of windows in my house is\n14. The number of doors is 6.\n": "NOT_TO_EQUAL",
|
||||
"The number of windows in my house is\n1. The number of doors is 6.\n": "TO_EQUAL",
|
||||
"- foo\n\n- bar\n\n\n- baz\n": "NOT_TO_EQUAL",
|
||||
"- foo\n - bar\n - baz\n\n\n bim\n": "NOT_TO_EQUAL",
|
||||
"- foo\n- bar\n\n<!-- -->\n\n- baz\n- bim\n": "TO_EQUAL",
|
||||
"- foo\n\n notcode\n\n- foo\n\n<!-- -->\n\n code\n": "NOT_TO_EQUAL",
|
||||
"- a\n - b\n - c\n - d\n - e\n - f\n - g\n - h\n- i\n": "NOT_TO_EQUAL",
|
||||
"1. a\n\n 2. b\n\n 3. c\n": "NOT_TO_EQUAL",
|
||||
"- a\n- b\n\n- c\n": "NOT_TO_EQUAL",
|
||||
"* a\n*\n\n* c\n": "TO_ERROR",
|
||||
"- a\n- b\n\n c\n- d\n": "NOT_TO_EQUAL",
|
||||
"- a\n- b\n\n [ref]: /url\n- d\n": "NOT_TO_EQUAL",
|
||||
"- a\n- ```\n b\n\n\n ```\n- c\n": "NOT_TO_EQUAL",
|
||||
"- a\n - b\n\n c\n- d\n": "NOT_TO_EQUAL",
|
||||
"* a\n > b\n >\n* c\n": "NOT_TO_EQUAL",
|
||||
"- a\n > b\n ```\n c\n ```\n- d\n": "NOT_TO_EQUAL",
|
||||
"- a\n": "TO_EQUAL",
|
||||
"- a\n - b\n": "NOT_TO_EQUAL",
|
||||
"1. ```\n foo\n ```\n\n bar\n": "TO_EQUAL",
|
||||
"* foo\n * bar\n\n baz\n": "NOT_TO_EQUAL",
|
||||
"- a\n - b\n - c\n\n- d\n - e\n - f\n": "TO_EQUAL",
|
||||
"`hi`lo`\n": "TO_EQUAL",
|
||||
"\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~\n": "NOT_TO_EQUAL",
|
||||
"\\\t\\A\\a\\ \\3\\φ\\«\n": "TO_EQUAL",
|
||||
"\\*not emphasized*\n\\<br/> not a tag\n\\[not a link](/foo)\n\\`not code`\n1\\. not a list\n\\* not a list\n\\# not a heading\n\\[foo]: /url \"not a reference\"\n": "NOT_TO_EQUAL",
|
||||
"\\\\*emphasis*\n": "NOT_TO_EQUAL",
|
||||
"foo\\\nbar\n": "NOT_TO_EQUAL",
|
||||
"`` \\[\\` ``\n": "TO_EQUAL",
|
||||
" \\[\\]\n": "TO_EQUAL",
|
||||
"~~~\n\\[\\]\n~~~\n": "TO_EQUAL",
|
||||
"<http://example.com?find=\\*>\n": "NOT_TO_EQUAL",
|
||||
"<a href=\"/bar\\/)\">\n": "TO_EQUAL",
|
||||
"[foo](/bar\\* \"ti\\*tle\")\n": "TO_EQUAL",
|
||||
"[foo]\n\n[foo]: /bar\\* \"ti\\*tle\"\n": "TO_EQUAL",
|
||||
"``` foo\\+bar\nfoo\n```\n": "TO_EQUAL",
|
||||
" & © Æ Ď\n¾ ℋ ⅆ\n∲ ≧̸\n": "NOT_TO_EQUAL",
|
||||
"# Ӓ Ϡ � �\n": "NOT_TO_EQUAL",
|
||||
"" ആ ಫ\n": "NOT_TO_EQUAL",
|
||||
"  &x; &#; &#x;\n&ThisIsNotDefined; &hi?;\n": "NOT_TO_EQUAL",
|
||||
"©\n": "NOT_TO_EQUAL",
|
||||
"&MadeUpEntity;\n": "NOT_TO_EQUAL",
|
||||
"<a href=\"öö.html\">\n": "TO_EQUAL",
|
||||
"[foo](/föö \"föö\")\n": "TO_EQUAL",
|
||||
"[foo]\n\n[foo]: /föö \"föö\"\n": "TO_EQUAL",
|
||||
"``` föö\nfoo\n```\n": "TO_EQUAL",
|
||||
"`föö`\n": "NOT_TO_EQUAL",
|
||||
" föfö\n": "NOT_TO_EQUAL",
|
||||
"`foo`\n": "TO_EQUAL",
|
||||
"`` foo ` bar ``\n": "TO_EQUAL",
|
||||
"` `` `\n": "TO_EQUAL",
|
||||
"`foo bar\n baz`\n": "TO_EQUAL",
|
||||
"`a b`\n": "NOT_TO_EQUAL",
|
||||
"`foo `` bar`\n": "TO_EQUAL",
|
||||
"`foo\\`bar`\n": "TO_EQUAL",
|
||||
"*foo`*`\n": "NOT_TO_EQUAL",
|
||||
"[not a `link](/foo`)\n": "NOT_TO_EQUAL",
|
||||
"`<a href=\"`\">`\n": "NOT_TO_EQUAL",
|
||||
"<a href=\"`\">`\n": "TO_EQUAL",
|
||||
"`<http://foo.bar.`baz>`\n": "NOT_TO_EQUAL",
|
||||
"<http://foo.bar.`baz>`\n": "TO_EQUAL",
|
||||
"```foo``\n": "NOT_TO_EQUAL",
|
||||
"`foo\n": "TO_EQUAL",
|
||||
"`foo``bar``\n": "NOT_TO_EQUAL",
|
||||
"*foo bar*\n": "TO_EQUAL",
|
||||
"a * foo bar*\n": "NOT_TO_EQUAL",
|
||||
"a*\"foo\"*\n": "NOT_TO_EQUAL",
|
||||
"* a *\n": "NOT_TO_EQUAL",
|
||||
"foo*bar*\n": "TO_EQUAL",
|
||||
"5*6*78\n": "NOT_TO_EQUAL",
|
||||
"_foo bar_\n": "TO_EQUAL",
|
||||
"_ foo bar_\n": "NOT_TO_EQUAL",
|
||||
"a_\"foo\"_\n": "NOT_TO_EQUAL",
|
||||
"foo_bar_\n": "NOT_TO_EQUAL",
|
||||
"5_6_78\n": "TO_EQUAL",
|
||||
"пристаням_стремятся_\n": "NOT_TO_EQUAL",
|
||||
"aa_\"bb\"_cc\n": "NOT_TO_EQUAL",
|
||||
"foo-_(bar)_\n": "TO_EQUAL",
|
||||
"_foo*\n": "TO_EQUAL",
|
||||
"*foo bar *\n": "NOT_TO_EQUAL",
|
||||
"*foo bar\n*\n": "NOT_TO_EQUAL",
|
||||
"*(*foo)\n": "NOT_TO_EQUAL",
|
||||
"*(*foo*)*\n": "NOT_TO_EQUAL",
|
||||
"*foo*bar\n": "NOT_TO_EQUAL",
|
||||
"_foo bar _\n": "NOT_TO_EQUAL",
|
||||
"_(_foo)\n": "TO_EQUAL",
|
||||
"_(_foo_)_\n": "NOT_TO_EQUAL",
|
||||
"_foo_bar\n": "TO_EQUAL",
|
||||
"_пристаням_стремятся\n": "NOT_TO_EQUAL",
|
||||
"_foo_bar_baz_\n": "TO_EQUAL",
|
||||
"_(bar)_.\n": "TO_EQUAL",
|
||||
"**foo bar**\n": "TO_EQUAL",
|
||||
"** foo bar**\n": "NOT_TO_EQUAL",
|
||||
"a**\"foo\"**\n": "NOT_TO_EQUAL",
|
||||
"foo**bar**\n": "TO_EQUAL",
|
||||
"__foo bar__\n": "TO_EQUAL",
|
||||
"__ foo bar__\n": "NOT_TO_EQUAL",
|
||||
"__\nfoo bar__\n": "NOT_TO_EQUAL",
|
||||
"a__\"foo\"__\n": "NOT_TO_EQUAL",
|
||||
"foo__bar__\n": "NOT_TO_EQUAL",
|
||||
"5__6__78\n": "NOT_TO_EQUAL",
|
||||
"пристаням__стремятся__\n": "NOT_TO_EQUAL",
|
||||
"__foo, __bar__, baz__\n": "NOT_TO_EQUAL",
|
||||
"foo-__(bar)__\n": "TO_EQUAL",
|
||||
"**foo bar **\n": "NOT_TO_EQUAL",
|
||||
"**(**foo)\n": "NOT_TO_EQUAL",
|
||||
"*(**foo**)*\n": "TO_EQUAL",
|
||||
"**Gomphocarpus (*Gomphocarpus physocarpus*, syn.\n*Asclepias physocarpa*)**\n": "TO_EQUAL",
|
||||
"**foo \"*bar*\" foo**\n": "NOT_TO_EQUAL",
|
||||
"**foo**bar\n": "TO_EQUAL",
|
||||
"__foo bar __\n": "NOT_TO_EQUAL",
|
||||
"__(__foo)\n": "NOT_TO_EQUAL",
|
||||
"_(__foo__)_\n": "TO_EQUAL",
|
||||
"__foo__bar\n": "NOT_TO_EQUAL",
|
||||
"__пристаням__стремятся\n": "NOT_TO_EQUAL",
|
||||
"__foo__bar__baz__\n": "NOT_TO_EQUAL",
|
||||
"__(bar)__.\n": "TO_EQUAL",
|
||||
"*foo [bar](/url)*\n": "TO_EQUAL",
|
||||
"*foo\nbar*\n": "TO_EQUAL",
|
||||
"_foo __bar__ baz_\n": "TO_EQUAL",
|
||||
"_foo _bar_ baz_\n": "NOT_TO_EQUAL",
|
||||
"__foo_ bar_\n": "NOT_TO_EQUAL",
|
||||
"*foo *bar**\n": "NOT_TO_EQUAL",
|
||||
"*foo **bar** baz*\n": "TO_EQUAL",
|
||||
"*foo**bar**baz*\n": "TO_EQUAL",
|
||||
"***foo** bar*\n": "NOT_TO_EQUAL",
|
||||
"*foo **bar***\n": "NOT_TO_EQUAL",
|
||||
"*foo**bar***\n": "NOT_TO_EQUAL",
|
||||
"*foo **bar *baz* bim** bop*\n": "NOT_TO_EQUAL",
|
||||
"*foo [*bar*](/url)*\n": "NOT_TO_EQUAL",
|
||||
"** is not an empty emphasis\n": "TO_EQUAL",
|
||||
"**** is not an empty strong emphasis\n": "TO_EQUAL",
|
||||
"**foo [bar](/url)**\n": "TO_EQUAL",
|
||||
"**foo\nbar**\n": "TO_EQUAL",
|
||||
"__foo _bar_ baz__\n": "TO_EQUAL",
|
||||
"__foo __bar__ baz__\n": "NOT_TO_EQUAL",
|
||||
"____foo__ bar__\n": "NOT_TO_EQUAL",
|
||||
"**foo **bar****\n": "NOT_TO_EQUAL",
|
||||
"**foo *bar* baz**\n": "TO_EQUAL",
|
||||
"**foo*bar*baz**\n": "NOT_TO_EQUAL",
|
||||
"***foo* bar**\n": "TO_EQUAL",
|
||||
"**foo *bar***\n": "TO_EQUAL",
|
||||
"**foo *bar **baz**\nbim* bop**\n": "NOT_TO_EQUAL",
|
||||
"**foo [*bar*](/url)**\n": "TO_EQUAL",
|
||||
"__ is not an empty emphasis\n": "TO_EQUAL",
|
||||
"____ is not an empty strong emphasis\n": "TO_EQUAL",
|
||||
"foo ***\n": "TO_EQUAL",
|
||||
"foo *\\**\n": "NOT_TO_EQUAL",
|
||||
"foo *_*\n": "NOT_TO_EQUAL",
|
||||
"foo *****\n": "NOT_TO_EQUAL",
|
||||
"foo **\\***\n": "TO_EQUAL",
|
||||
"foo **_**\n": "TO_EQUAL",
|
||||
"**foo*\n": "TO_EQUAL",
|
||||
"*foo**\n": "NOT_TO_EQUAL",
|
||||
"***foo**\n": "NOT_TO_EQUAL",
|
||||
"****foo*\n": "NOT_TO_EQUAL",
|
||||
"**foo***\n": "NOT_TO_EQUAL",
|
||||
"*foo****\n": "NOT_TO_EQUAL",
|
||||
"foo ___\n": "TO_EQUAL",
|
||||
"foo _\\__\n": "NOT_TO_EQUAL",
|
||||
"foo _*_\n": "TO_EQUAL",
|
||||
"foo _____\n": "NOT_TO_EQUAL",
|
||||
"foo __\\___\n": "TO_EQUAL",
|
||||
"foo __*__\n": "TO_EQUAL",
|
||||
"__foo_\n": "TO_EQUAL",
|
||||
"_foo__\n": "NOT_TO_EQUAL",
|
||||
"___foo__\n": "NOT_TO_EQUAL",
|
||||
"____foo_\n": "NOT_TO_EQUAL",
|
||||
"__foo___\n": "NOT_TO_EQUAL",
|
||||
"_foo____\n": "NOT_TO_EQUAL",
|
||||
"**foo**\n": "TO_EQUAL",
|
||||
"*_foo_*\n": "NOT_TO_EQUAL",
|
||||
"__foo__\n": "TO_EQUAL",
|
||||
"_*foo*_\n": "NOT_TO_EQUAL",
|
||||
"****foo****\n": "NOT_TO_EQUAL",
|
||||
"____foo____\n": "NOT_TO_EQUAL",
|
||||
"******foo******\n": "NOT_TO_EQUAL",
|
||||
"***foo***\n": "TO_EQUAL",
|
||||
"_____foo_____\n": "NOT_TO_EQUAL",
|
||||
"*foo _bar* baz_\n": "TO_EQUAL",
|
||||
"*foo __bar *baz bim__ bam*\n": "NOT_TO_EQUAL",
|
||||
"**foo **bar baz**\n": "NOT_TO_EQUAL",
|
||||
"*foo *bar baz*\n": "NOT_TO_EQUAL",
|
||||
"*[bar*](/url)\n": "NOT_TO_EQUAL",
|
||||
"_foo [bar_](/url)\n": "NOT_TO_EQUAL",
|
||||
"*<img src=\"foo\" title=\"*\"/>\n": "NOT_TO_EQUAL",
|
||||
"**<a href=\"**\">\n": "NOT_TO_EQUAL",
|
||||
"__<a href=\"__\">\n": "NOT_TO_EQUAL",
|
||||
"*a `*`*\n": "NOT_TO_EQUAL",
|
||||
"_a `_`_\n": "NOT_TO_EQUAL",
|
||||
"**a<http://foo.bar/?q=**>\n": "NOT_TO_EQUAL",
|
||||
"__a<http://foo.bar/?q=__>\n": "NOT_TO_EQUAL",
|
||||
"[link](/uri \"title\")\n": "TO_EQUAL",
|
||||
"[link](/uri)\n": "TO_EQUAL",
|
||||
"[link]()\n": "TO_EQUAL",
|
||||
"[link](<>)\n": "TO_EQUAL",
|
||||
"[link](/my uri)\n": "TO_EQUAL",
|
||||
"[link](</my uri>)\n": "NOT_TO_EQUAL",
|
||||
"[link](foo\nbar)\n": "TO_EQUAL",
|
||||
"[link](<foo\nbar>)\n": "TO_EQUAL",
|
||||
"[link](\\(foo\\))\n": "TO_EQUAL",
|
||||
"[link](foo(and(bar)))\n": "TO_EQUAL",
|
||||
"[link](foo\\(and\\(bar\\))\n": "TO_EQUAL",
|
||||
"[link](<foo(and(bar)>)\n": "TO_EQUAL",
|
||||
"[link](foo\\)\\:)\n": "TO_EQUAL",
|
||||
"[link](#fragment)\n\n[link](http://example.com#fragment)\n\n[link](http://example.com?foo=3#frag)\n": "TO_EQUAL",
|
||||
"[link](foo\\bar)\n": "TO_EQUAL",
|
||||
"[link](foo%20bä)\n": "TO_EQUAL",
|
||||
"[link](\"title\")\n": "TO_EQUAL",
|
||||
"[link](/url \"title\")\n[link](/url 'title')\n[link](/url (title))\n": "TO_EQUAL",
|
||||
"[link](/url \"title \\\""\")\n": "NOT_TO_EQUAL",
|
||||
"[link](/url \"title\")\n": "NOT_TO_EQUAL",
|
||||
"[link](/url \"title \"and\" title\")\n": "NOT_TO_EQUAL",
|
||||
"[link](/url 'title \"and\" title')\n": "NOT_TO_EQUAL",
|
||||
"[link]( /uri\n \"title\" )\n": "TO_EQUAL",
|
||||
"[link] (/uri)\n": "NOT_TO_EQUAL",
|
||||
"[link [foo [bar]]](/uri)\n": "NOT_TO_EQUAL",
|
||||
"[link] bar](/uri)\n": "TO_EQUAL",
|
||||
"[link [bar](/uri)\n": "TO_EQUAL",
|
||||
"[link \\[bar](/uri)\n": "NOT_TO_EQUAL",
|
||||
"[link *foo **bar** `#`*](/uri)\n": "TO_EQUAL",
|
||||
"[](/uri)\n": "NOT_TO_EQUAL",
|
||||
"[foo [bar](/uri)](/uri)\n": "NOT_TO_EQUAL",
|
||||
"[foo *[bar [baz](/uri)](/uri)*](/uri)\n": "NOT_TO_EQUAL",
|
||||
"](uri2)](uri3)\n": "NOT_TO_EQUAL",
|
||||
"*[foo*](/uri)\n": "NOT_TO_EQUAL",
|
||||
"[foo *bar](baz*)\n": "TO_EQUAL",
|
||||
"*foo [bar* baz]\n": "TO_EQUAL",
|
||||
"[foo <bar attr=\"](baz)\">\n": "NOT_TO_EQUAL",
|
||||
"[foo`](/uri)`\n": "NOT_TO_EQUAL",
|
||||
"[foo<http://example.com/?search=](uri)>\n": "NOT_TO_EQUAL",
|
||||
"[foo][bar]\n\n[bar]: /url \"title\"\n": "TO_EQUAL",
|
||||
"[link [foo [bar]]][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL",
|
||||
"[link \\[bar][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL",
|
||||
"[link *foo **bar** `#`*][ref]\n\n[ref]: /uri\n": "TO_EQUAL",
|
||||
"[][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL",
|
||||
"[foo [bar](/uri)][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL",
|
||||
"[foo *bar [baz][ref]*][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL",
|
||||
"*[foo*][ref]\n\n[ref]: /uri\n": "NOT_TO_EQUAL",
|
||||
"[foo *bar][ref]\n\n[ref]: /uri\n": "TO_EQUAL",
|
||||
"[foo <bar attr=\"][ref]\">\n\n[ref]: /uri\n": "NOT_TO_EQUAL",
|
||||
"[foo`][ref]`\n\n[ref]: /uri\n": "NOT_TO_EQUAL",
|
||||
"[foo<http://example.com/?search=][ref]>\n\n[ref]: /uri\n": "NOT_TO_EQUAL",
|
||||
"[foo][BaR]\n\n[bar]: /url \"title\"\n": "TO_EQUAL",
|
||||
"[Толпой][Толпой] is a Russian word.\n\n[ТОЛПОЙ]: /url\n": "TO_EQUAL",
|
||||
"[Foo\n bar]: /url\n\n[Baz][Foo bar]\n": "TO_EQUAL",
|
||||
"[foo] [bar]\n\n[bar]: /url \"title\"\n": "NOT_TO_EQUAL",
|
||||
"[foo]\n[bar]\n\n[bar]: /url \"title\"\n": "NOT_TO_EQUAL",
|
||||
"[foo]: /url1\n\n[foo]: /url2\n\n[bar][foo]\n": "NOT_TO_EQUAL",
|
||||
"[bar][foo\\!]\n\n[foo!]: /url\n": "NOT_TO_EQUAL",
|
||||
"[foo][ref[]\n\n[ref[]: /uri\n": "NOT_TO_EQUAL",
|
||||
"[foo][ref[bar]]\n\n[ref[bar]]: /uri\n": "NOT_TO_EQUAL",
|
||||
"[[[foo]]]\n\n[[[foo]]]: /url\n": "TO_EQUAL",
|
||||
"[foo][ref\\[]\n\n[ref\\[]: /uri\n": "TO_EQUAL",
|
||||
"[bar\\\\]: /uri\n\n[bar\\\\]\n": "NOT_TO_EQUAL",
|
||||
"[]\n\n[]: /uri\n": "TO_EQUAL",
|
||||
"[\n ]\n\n[\n ]: /uri\n": "NOT_TO_EQUAL",
|
||||
"[foo][]\n\n[foo]: /url \"title\"\n": "TO_EQUAL",
|
||||
"[*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n": "TO_EQUAL",
|
||||
"[Foo][]\n\n[foo]: /url \"title\"\n": "TO_EQUAL",
|
||||
"[foo] \n[]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL",
|
||||
"[foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL",
|
||||
"[*foo* bar]\n\n[*foo* bar]: /url \"title\"\n": "TO_EQUAL",
|
||||
"[[*foo* bar]]\n\n[*foo* bar]: /url \"title\"\n": "TO_EQUAL",
|
||||
"[[bar [foo]\n\n[foo]: /url\n": "TO_EQUAL",
|
||||
"[Foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL",
|
||||
"[foo] bar\n\n[foo]: /url\n": "TO_EQUAL",
|
||||
"\\[foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL",
|
||||
"[foo*]: /url\n\n*[foo*]\n": "NOT_TO_EQUAL",
|
||||
"[foo][bar]\n\n[foo]: /url1\n[bar]: /url2\n": "TO_EQUAL",
|
||||
"[foo][]\n\n[foo]: /url1\n": "TO_EQUAL",
|
||||
"[foo]()\n\n[foo]: /url1\n": "TO_EQUAL",
|
||||
"[foo](not a link)\n\n[foo]: /url1\n": "TO_EQUAL",
|
||||
"[foo][bar][baz]\n\n[baz]: /url\n": "NOT_TO_EQUAL",
|
||||
"[foo][bar][baz]\n\n[baz]: /url1\n[bar]: /url2\n": "TO_EQUAL",
|
||||
"[foo][bar][baz]\n\n[baz]: /url1\n[foo]: /url2\n": "NOT_TO_EQUAL",
|
||||
"\n": "NOT_TO_EQUAL",
|
||||
"![foo *bar*]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n": "NOT_TO_EQUAL",
|
||||
"](/url2)\n": "NOT_TO_EQUAL",
|
||||
"](/url2)\n": "NOT_TO_EQUAL",
|
||||
"![foo *bar*][]\n\n[foo *bar*]: train.jpg \"train & tracks\"\n": "NOT_TO_EQUAL",
|
||||
"![foo *bar*][foobar]\n\n[FOOBAR]: train.jpg \"train & tracks\"\n": "NOT_TO_EQUAL",
|
||||
"\n": "NOT_TO_EQUAL",
|
||||
"My \n": "NOT_TO_EQUAL",
|
||||
"\n": "NOT_TO_EQUAL",
|
||||
"\n": "NOT_TO_EQUAL",
|
||||
"![foo][bar]\n\n[bar]: /url\n": "NOT_TO_EQUAL",
|
||||
"![foo][bar]\n\n[BAR]: /url\n": "NOT_TO_EQUAL",
|
||||
"![foo][]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL",
|
||||
"![*foo* bar][]\n\n[*foo* bar]: /url \"title\"\n": "NOT_TO_EQUAL",
|
||||
"![Foo][]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL",
|
||||
"![foo] \n[]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL",
|
||||
"![foo]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL",
|
||||
"![*foo* bar]\n\n[*foo* bar]: /url \"title\"\n": "NOT_TO_EQUAL",
|
||||
"![[foo]]\n\n[[foo]]: /url \"title\"\n": "NOT_TO_EQUAL",
|
||||
"![Foo]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL",
|
||||
"!\\[foo]\n\n[foo]: /url \"title\"\n": "TO_EQUAL",
|
||||
"\\![foo]\n\n[foo]: /url \"title\"\n": "NOT_TO_EQUAL",
|
||||
"<http://foo.bar.baz>\n": "TO_EQUAL",
|
||||
"<http://foo.bar.baz/test?q=hello&id=22&boolean>\n": "NOT_TO_EQUAL",
|
||||
"<irc://foo.bar:2233/baz>\n": "TO_EQUAL",
|
||||
"<MAILTO:FOO@BAR.BAZ>\n": "NOT_TO_EQUAL",
|
||||
"<a+b+c:d>\n": "NOT_TO_EQUAL",
|
||||
"<made-up-scheme://foo,bar>\n": "TO_EQUAL",
|
||||
"<http://../>\n": "TO_EQUAL",
|
||||
"<localhost:5001/foo>\n": "NOT_TO_EQUAL",
|
||||
"<http://foo.bar/baz bim>\n": "NOT_TO_EQUAL",
|
||||
"<http://example.com/\\[\\>\n": "NOT_TO_EQUAL",
|
||||
"<foo@bar.example.com>\n": "TO_EQUAL",
|
||||
"<foo+special@Bar.baz-bar0.com>\n": "TO_EQUAL",
|
||||
"<foo\\+@bar.example.com>\n": "NOT_TO_EQUAL",
|
||||
"<>\n": "NOT_TO_EQUAL",
|
||||
"< http://foo.bar >\n": "NOT_TO_EQUAL",
|
||||
"<m:abc>\n": "NOT_TO_EQUAL",
|
||||
"<foo.bar.baz>\n": "NOT_TO_EQUAL",
|
||||
"http://example.com\n": "TO_EQUAL",
|
||||
"foo@bar.example.com\n": "TO_EQUAL",
|
||||
"<a><bab><c2c>\n": "TO_EQUAL",
|
||||
"<a/><b2/>\n": "TO_EQUAL",
|
||||
"<a /><b2\ndata=\"foo\" >\n": "TO_EQUAL",
|
||||
"<a foo=\"bar\" bam = 'baz <em>\"</em>'\n_boolean zoop:33=zoop:33 />\n": "TO_EQUAL",
|
||||
"Foo <responsive-image src=\"foo.jpg\" />\n": "TO_EQUAL",
|
||||
"<33> <__>\n": "NOT_TO_EQUAL",
|
||||
"<a h*#ref=\"hi\">\n": "NOT_TO_EQUAL",
|
||||
"<a href=\"hi'> <a href=hi'>\n": "NOT_TO_EQUAL",
|
||||
"< a><\nfoo><bar/ >\n": "NOT_TO_EQUAL",
|
||||
"<a href='bar'title=title>\n": "NOT_TO_EQUAL",
|
||||
"</a></foo >\n": "TO_EQUAL",
|
||||
"</a href=\"foo\">\n": "NOT_TO_EQUAL",
|
||||
"foo <!-- this is a\ncomment - with hyphen -->\n": "TO_EQUAL",
|
||||
"foo <!-- not a comment -- two hyphens -->\n": "NOT_TO_EQUAL",
|
||||
"foo <!--> foo -->\n\nfoo <!-- foo--->\n": "NOT_TO_EQUAL",
|
||||
"foo <?php echo $a; ?>\n": "TO_EQUAL",
|
||||
"foo <!ELEMENT br EMPTY>\n": "TO_EQUAL",
|
||||
"foo <![CDATA[>&<]]>\n": "NOT_TO_EQUAL",
|
||||
"foo <a href=\"ö\">\n": "TO_EQUAL",
|
||||
"foo <a href=\"\\*\">\n": "TO_EQUAL",
|
||||
"<a href=\"\\\"\">\n": "NOT_TO_EQUAL",
|
||||
"foo \nbaz\n": "NOT_TO_EQUAL",
|
||||
"foo\\\nbaz\n": "NOT_TO_EQUAL",
|
||||
"foo \nbaz\n": "NOT_TO_EQUAL",
|
||||
"foo \n bar\n": "NOT_TO_EQUAL",
|
||||
"foo\\\n bar\n": "NOT_TO_EQUAL",
|
||||
"*foo \nbar*\n": "NOT_TO_EQUAL",
|
||||
"*foo\\\nbar*\n": "NOT_TO_EQUAL",
|
||||
"`code \nspan`\n": "TO_EQUAL",
|
||||
"`code\\\nspan`\n": "TO_EQUAL",
|
||||
"<a href=\"foo \nbar\">\n": "TO_EQUAL",
|
||||
"<a href=\"foo\\\nbar\">\n": "TO_EQUAL",
|
||||
"foo\\\n": "TO_EQUAL",
|
||||
"foo \n": "TO_EQUAL",
|
||||
"### foo\\\n": "TO_EQUAL",
|
||||
"### foo \n": "TO_EQUAL",
|
||||
"foo\nbaz\n": "TO_EQUAL",
|
||||
"foo \n baz\n": "TO_EQUAL",
|
||||
"hello $.;'there\n": "TO_EQUAL",
|
||||
"Foo χρῆν\n": "TO_EQUAL",
|
||||
"Multiple spaces\n": "TO_EQUAL"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Fill to_*this*_mark, and your charge is but a penny; to_*this*_a penny more; and so on to the full glass—the Cape Horn measure, which you may gulp down for a shilling.\n\nUpon entering the place I found a number of young seamen gathered about a table, examining by a dim light divers specimens of_*skrimshander*.
|
||||
@@ -0,0 +1,132 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`remarkParseShortcodes parse pattern with leading caret should be a remark shortcode node 1`] = `
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
"shortcode": "foo",
|
||||
"shortcodeData": Object {
|
||||
"bar": "baz",
|
||||
},
|
||||
},
|
||||
"type": "shortcode",
|
||||
},
|
||||
],
|
||||
"type": "root",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`remarkParseShortcodes parse pattern with leading caret should parse multiple shortcodes 1`] = `
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"type": "text",
|
||||
"value": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"shortcode": "foo",
|
||||
"shortcodeData": Object {
|
||||
"bar": "bar",
|
||||
},
|
||||
},
|
||||
"type": "shortcode",
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"shortcode": "foo",
|
||||
"shortcodeData": Object {
|
||||
"bar": "baz",
|
||||
},
|
||||
},
|
||||
"type": "shortcode",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"type": "text",
|
||||
"value": "next para",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "root",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`remarkParseShortcodes parse pattern without leading caret should handle pattern without leading caret 1`] = `
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"type": "text",
|
||||
"value": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"shortcode": "foo",
|
||||
"shortcodeData": Object {
|
||||
"bar": "baz",
|
||||
},
|
||||
},
|
||||
"type": "shortcode",
|
||||
},
|
||||
],
|
||||
"type": "root",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`remarkParseShortcodes parse pattern without leading caret should parse multiple shortcodes 1`] = `
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"type": "text",
|
||||
"value": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"shortcode": "foo",
|
||||
"shortcodeData": Object {
|
||||
"bar": "bar",
|
||||
},
|
||||
},
|
||||
"type": "shortcode",
|
||||
},
|
||||
Object {
|
||||
"data": Object {
|
||||
"shortcode": "foo",
|
||||
"shortcodeData": Object {
|
||||
"bar": "baz",
|
||||
},
|
||||
},
|
||||
"type": "shortcode",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"type": "text",
|
||||
"value": "next para",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "root",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,110 @@
|
||||
import flow from 'lodash/flow';
|
||||
import { tests as commonmarkSpec } from 'commonmark-spec';
|
||||
import * as commonmark from 'commonmark';
|
||||
|
||||
import { markdownToSlate, slateToMarkdown } from '../index.js';
|
||||
|
||||
const skips = [
|
||||
{
|
||||
number: [456],
|
||||
reason: 'Remark ¯\\_(ツ)_/¯',
|
||||
},
|
||||
{
|
||||
number: [416, 417, 424, 425, 426, 431, 457, 460, 462, 464, 467],
|
||||
reason: 'Remark does not support infinite (redundant) nested marks',
|
||||
},
|
||||
{
|
||||
number: [455, 469, 470, 471],
|
||||
reason: 'Remark parses the initial set of identical nested delimiters first',
|
||||
},
|
||||
{
|
||||
number: [473, 476, 478, 480],
|
||||
reason: 'we convert underscores to asterisks for strong/emphasis',
|
||||
},
|
||||
{ number: 490, reason: 'Remark strips pointy enclosing pointy brackets from link url' },
|
||||
{ number: 503, reason: 'Remark allows non-breaking space between link url and title' },
|
||||
{ number: 507, reason: 'Remark allows a space between link alt and url' },
|
||||
{
|
||||
number: [
|
||||
511, 516, 525, 528, 529, 530, 532, 533, 534, 540, 541, 542, 543, 546, 548, 560, 565, 567,
|
||||
],
|
||||
reason: 'we convert link references to standard links, but Remark also fails these',
|
||||
},
|
||||
{
|
||||
number: [569, 570, 571, 572, 573, 581, 585],
|
||||
reason: 'Remark does not recognize or remove marks in image alt text',
|
||||
},
|
||||
{ number: 589, reason: 'Remark does not honor backslash escape of image exclamation point' },
|
||||
{ number: 593, reason: 'Remark removes "mailto:" from autolink text' },
|
||||
{ number: 599, reason: 'Remark does not escape all expected entities' },
|
||||
{ number: 602, reason: 'Remark allows autolink emails to contain backslashes' },
|
||||
];
|
||||
|
||||
const onlys = [
|
||||
// just add the spec number, eg:
|
||||
// 431,
|
||||
];
|
||||
|
||||
/**
|
||||
* Each test receives input markdown and output html as expected for Commonmark
|
||||
* compliance. To test all of our handling in one go, we serialize the markdown
|
||||
* into our Slate AST, then back to raw markdown, and finally to HTML.
|
||||
*/
|
||||
const reader = new commonmark.Parser();
|
||||
const writer = new commonmark.HtmlRenderer();
|
||||
|
||||
function parseWithCommonmark(markdown) {
|
||||
const parsed = reader.parse(markdown);
|
||||
return writer.render(parsed);
|
||||
}
|
||||
|
||||
const parse = flow([markdownToSlate, slateToMarkdown]);
|
||||
|
||||
/**
|
||||
* Passing this test suite requires 100% Commonmark compliance. There are 624
|
||||
* tests, of which we're passing about 300 as of introduction of this suite. To
|
||||
* work on improving Commonmark support, update __fixtures__/commonmarkExpected.json
|
||||
*/
|
||||
describe.skip('Commonmark support', function () {
|
||||
const specs =
|
||||
onlys.length > 0
|
||||
? commonmarkSpec.filter(({ number }) => onlys.includes(number))
|
||||
: commonmarkSpec;
|
||||
specs.forEach(spec => {
|
||||
const skip = skips.find(({ number }) => {
|
||||
return Array.isArray(number) ? number.includes(spec.number) : number === spec.number;
|
||||
});
|
||||
const specUrl = `https://spec.commonmark.org/0.29/#example-${spec.number}`;
|
||||
const parsed = parse(spec.markdown);
|
||||
const commonmarkParsedHtml = parseWithCommonmark(parsed);
|
||||
const description = `
|
||||
${spec.section}
|
||||
${specUrl}
|
||||
|
||||
Spec:
|
||||
${JSON.stringify(spec, null, 2)}
|
||||
|
||||
Markdown input:
|
||||
${spec.markdown}
|
||||
|
||||
Markdown parsed through Slate/Remark and back to Markdown:
|
||||
${parsed}
|
||||
|
||||
HTML output:
|
||||
${commonmarkParsedHtml}
|
||||
|
||||
Expected HTML output:
|
||||
${spec.html}
|
||||
`;
|
||||
if (skip) {
|
||||
const showMessage = Array.isArray(skip.number) ? skip.number[0] === spec.number : true;
|
||||
if (showMessage) {
|
||||
//console.log(`skipping spec ${skip.number}\n${skip.reason}\n${specUrl}`);
|
||||
}
|
||||
}
|
||||
const testFn = skip ? test.skip : test;
|
||||
testFn(description, () => {
|
||||
expect(commonmarkParsedHtml).toEqual(spec.html);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
import { markdownToSlate, htmlToSlate } from '../';
|
||||
|
||||
describe('markdownToSlate', () => {
|
||||
it('should not add duplicate identical marks under the same node (GitHub Issue 3280)', () => {
|
||||
const mdast = fs.readFileSync(
|
||||
path.join(__dirname, '__fixtures__', 'duplicate_marks_github_issue_3280.md'),
|
||||
);
|
||||
const slate = markdownToSlate(mdast);
|
||||
|
||||
expect(slate).toEqual([
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
text: 'Fill to',
|
||||
},
|
||||
{
|
||||
italic: true,
|
||||
marks: [{ type: 'italic' }],
|
||||
text: 'this_mark, and your charge is but a penny; tothisa penny more; and so on to the full glass—the Cape Horn measure, which you may gulp down for a shilling.\\n\\nUpon entering the place I found a number of young seamen gathered about a table, examining by a dim light divers specimens ofskrimshander',
|
||||
},
|
||||
{
|
||||
text: '.',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('htmlToSlate', () => {
|
||||
it('should preserve spaces in rich html (GitHub Issue 3727)', () => {
|
||||
const html = `<strong>Bold Text</strong><span><span> </span>regular text<span> </span></span>`;
|
||||
|
||||
const actual = htmlToSlate(html);
|
||||
expect(actual).toEqual({
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{ text: 'Bold Text', bold: true, marks: [{ type: 'bold' }] },
|
||||
{ text: ' regular text' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import unified from 'unified';
|
||||
import markdownToRemark from 'remark-parse';
|
||||
|
||||
import remarkAllowHtmlEntities from '../remarkAllowHtmlEntities';
|
||||
|
||||
function process(markdown) {
|
||||
const mdast = unified().use(markdownToRemark).use(remarkAllowHtmlEntities).parse(markdown);
|
||||
|
||||
/**
|
||||
* The MDAST will look like:
|
||||
*
|
||||
* { type: 'root', children: [
|
||||
* { type: 'paragraph', children: [
|
||||
* // results here
|
||||
* ]}
|
||||
* ]}
|
||||
*/
|
||||
return mdast.children[0].children[0].value;
|
||||
}
|
||||
|
||||
describe('remarkAllowHtmlEntities', () => {
|
||||
it('should not decode HTML entities', () => {
|
||||
expect(process('<div>')).toEqual('<div>');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,171 @@
|
||||
import u from 'unist-builder';
|
||||
|
||||
import remarkAssertParents from '../remarkAssertParents';
|
||||
|
||||
const transform = remarkAssertParents();
|
||||
|
||||
describe('remarkAssertParents', () => {
|
||||
it('should unnest invalidly nested blocks', () => {
|
||||
const input = u('root', [
|
||||
u('paragraph', [
|
||||
u('paragraph', [u('text', 'Paragraph text.')]),
|
||||
u('heading', { depth: 1 }, [u('text', 'Heading text.')]),
|
||||
u('code', 'someCode()'),
|
||||
u('blockquote', [u('text', 'Quote text.')]),
|
||||
u('list', [u('listItem', [u('text', 'A list item.')])]),
|
||||
u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]),
|
||||
u('thematicBreak'),
|
||||
]),
|
||||
]);
|
||||
|
||||
const output = u('root', [
|
||||
u('paragraph', [u('text', 'Paragraph text.')]),
|
||||
u('heading', { depth: 1 }, [u('text', 'Heading text.')]),
|
||||
u('code', 'someCode()'),
|
||||
u('blockquote', [u('text', 'Quote text.')]),
|
||||
u('list', [u('listItem', [u('text', 'A list item.')])]),
|
||||
u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]),
|
||||
u('thematicBreak'),
|
||||
]);
|
||||
|
||||
expect(transform(input)).toEqual(output);
|
||||
});
|
||||
|
||||
it('should unnest deeply nested blocks', () => {
|
||||
const input = u('root', [
|
||||
u('paragraph', [
|
||||
u('paragraph', [
|
||||
u('paragraph', [
|
||||
u('paragraph', [u('text', 'Paragraph text.')]),
|
||||
u('heading', { depth: 1 }, [u('text', 'Heading text.')]),
|
||||
u('code', 'someCode()'),
|
||||
u('blockquote', [
|
||||
u('paragraph', [u('strong', [u('heading', [u('text', 'Quote text.')])])]),
|
||||
]),
|
||||
u('list', [u('listItem', [u('text', 'A list item.')])]),
|
||||
u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]),
|
||||
u('thematicBreak'),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
const output = u('root', [
|
||||
u('paragraph', [u('text', 'Paragraph text.')]),
|
||||
u('heading', { depth: 1 }, [u('text', 'Heading text.')]),
|
||||
u('code', 'someCode()'),
|
||||
u('blockquote', [u('heading', [u('text', 'Quote text.')])]),
|
||||
u('list', [u('listItem', [u('text', 'A list item.')])]),
|
||||
u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]),
|
||||
u('thematicBreak'),
|
||||
]);
|
||||
|
||||
expect(transform(input)).toEqual(output);
|
||||
});
|
||||
|
||||
it('should remove blocks that are emptied as a result of denesting', () => {
|
||||
const input = u('root', [
|
||||
u('paragraph', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]),
|
||||
]);
|
||||
|
||||
const output = u('root', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]);
|
||||
|
||||
expect(transform(input)).toEqual(output);
|
||||
});
|
||||
|
||||
it('should remove blocks that are emptied as a result of denesting', () => {
|
||||
const input = u('root', [
|
||||
u('paragraph', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]),
|
||||
]);
|
||||
|
||||
const output = u('root', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]);
|
||||
|
||||
expect(transform(input)).toEqual(output);
|
||||
});
|
||||
|
||||
it('should handle asymmetrical splits', () => {
|
||||
const input = u('root', [
|
||||
u('paragraph', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]),
|
||||
]);
|
||||
|
||||
const output = u('root', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]);
|
||||
|
||||
expect(transform(input)).toEqual(output);
|
||||
});
|
||||
|
||||
it('should nest invalidly nested blocks in the nearest valid ancestor', () => {
|
||||
const input = u('root', [
|
||||
u('paragraph', [
|
||||
u('blockquote', [u('strong', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])])]),
|
||||
]),
|
||||
]);
|
||||
|
||||
const output = u('root', [
|
||||
u('blockquote', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]),
|
||||
]);
|
||||
|
||||
expect(transform(input)).toEqual(output);
|
||||
});
|
||||
|
||||
it('should preserve validly nested siblings of invalidly nested blocks', () => {
|
||||
const input = u('root', [
|
||||
u('paragraph', [
|
||||
u('blockquote', [
|
||||
u('strong', [
|
||||
u('text', 'Deep validly nested text a.'),
|
||||
u('heading', { depth: 1 }, [u('text', 'Heading text.')]),
|
||||
u('text', 'Deep validly nested text b.'),
|
||||
]),
|
||||
]),
|
||||
u('text', 'Validly nested text.'),
|
||||
]),
|
||||
]);
|
||||
|
||||
const output = u('root', [
|
||||
u('blockquote', [
|
||||
u('strong', [u('text', 'Deep validly nested text a.')]),
|
||||
u('heading', { depth: 1 }, [u('text', 'Heading text.')]),
|
||||
u('strong', [u('text', 'Deep validly nested text b.')]),
|
||||
]),
|
||||
u('paragraph', [u('text', 'Validly nested text.')]),
|
||||
]);
|
||||
|
||||
expect(transform(input)).toEqual(output);
|
||||
});
|
||||
|
||||
it('should allow intermediate parents like list and table to contain required block children', () => {
|
||||
const input = u('root', [
|
||||
u('blockquote', [
|
||||
u('list', [
|
||||
u('listItem', [
|
||||
u('table', [
|
||||
u('tableRow', [
|
||||
u('tableCell', [
|
||||
u('heading', { depth: 1 }, [u('text', 'Validly nested heading text.')]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
const output = u('root', [
|
||||
u('blockquote', [
|
||||
u('list', [
|
||||
u('listItem', [
|
||||
u('table', [
|
||||
u('tableRow', [
|
||||
u('tableCell', [
|
||||
u('heading', { depth: 1 }, [u('text', 'Validly nested heading text.')]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
expect(transform(input)).toEqual(output);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import unified from 'unified';
|
||||
import u from 'unist-builder';
|
||||
|
||||
import remarkEscapeMarkdownEntities from '../remarkEscapeMarkdownEntities';
|
||||
|
||||
function process(text) {
|
||||
const tree = u('root', [u('text', text)]);
|
||||
const escapedMdast = unified().use(remarkEscapeMarkdownEntities).runSync(tree);
|
||||
|
||||
return escapedMdast.children[0].value;
|
||||
}
|
||||
|
||||
describe('remarkEscapeMarkdownEntities', () => {
|
||||
it('should escape common markdown entities', () => {
|
||||
expect(process('*a*')).toEqual('\\*a\\*');
|
||||
expect(process('**a**')).toEqual('\\*\\*a\\*\\*');
|
||||
expect(process('***a***')).toEqual('\\*\\*\\*a\\*\\*\\*');
|
||||
expect(process('_a_')).toEqual('\\_a\\_');
|
||||
expect(process('__a__')).toEqual('\\_\\_a\\_\\_');
|
||||
expect(process('~~a~~')).toEqual('\\~\\~a\\~\\~');
|
||||
expect(process('[]')).toEqual('\\[]');
|
||||
expect(process('[]()')).toEqual('\\[]()');
|
||||
expect(process('[a](b)')).toEqual('\\[a](b)');
|
||||
expect(process('[Test sentence.](https://www.example.com)')).toEqual(
|
||||
'\\[Test sentence.](https://www.example.com)',
|
||||
);
|
||||
expect(process('')).toEqual('!\\[a](b)');
|
||||
});
|
||||
|
||||
it('should not escape inactive, single markdown entities', () => {
|
||||
expect(process('a*b')).toEqual('a*b');
|
||||
expect(process('_')).toEqual('_');
|
||||
expect(process('~')).toEqual('~');
|
||||
expect(process('[')).toEqual('[');
|
||||
});
|
||||
|
||||
it('should escape leading markdown entities', () => {
|
||||
expect(process('#')).toEqual('\\#');
|
||||
expect(process('-')).toEqual('\\-');
|
||||
expect(process('*')).toEqual('\\*');
|
||||
expect(process('>')).toEqual('\\>');
|
||||
expect(process('=')).toEqual('\\=');
|
||||
expect(process('|')).toEqual('\\|');
|
||||
expect(process('```')).toEqual('\\`\\``');
|
||||
expect(process(' ')).toEqual('\\ ');
|
||||
});
|
||||
|
||||
it('should escape leading markdown entities preceded by whitespace', () => {
|
||||
expect(process('\n #')).toEqual('\\#');
|
||||
expect(process(' \n-')).toEqual('\\-');
|
||||
});
|
||||
|
||||
it('should not escape leading markdown entities preceded by non-whitespace characters', () => {
|
||||
expect(process('a# # b #')).toEqual('a# # b #');
|
||||
expect(process('a- - b -')).toEqual('a- - b -');
|
||||
});
|
||||
|
||||
it('should not escape html tags', () => {
|
||||
expect(process('<a attr="**a**">')).toEqual('<a attr="**a**">');
|
||||
expect(process('a b <c attr="**d**"> e')).toEqual('a b <c attr="**d**"> e');
|
||||
});
|
||||
|
||||
it('should escape the contents of html blocks', () => {
|
||||
expect(process('<div>*a*</div>')).toEqual('<div>\\*a\\*</div>');
|
||||
});
|
||||
|
||||
it('should not escape the contents of preformatted html blocks', () => {
|
||||
expect(process('<pre>*a*</pre>')).toEqual('<pre>*a*</pre>');
|
||||
expect(process('<script>*a*</script>')).toEqual('<script>*a*</script>');
|
||||
expect(process('<style>*a*</style>')).toEqual('<style>*a*</style>');
|
||||
expect(process('<pre>\n*a*\n</pre>')).toEqual('<pre>\n*a*\n</pre>');
|
||||
expect(process('a b <pre>*c*</pre> d e')).toEqual('a b <pre>*c*</pre> d e');
|
||||
});
|
||||
|
||||
it('should not escape footnote references', () => {
|
||||
expect(process('[^a]')).toEqual('[^a]');
|
||||
expect(process('[^1]')).toEqual('[^1]');
|
||||
});
|
||||
|
||||
it('should not escape footnotes', () => {
|
||||
expect(process('[^a]:')).toEqual('[^a]:');
|
||||
expect(process('[^1]:')).toEqual('[^1]:');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import unified from 'unified';
|
||||
import markdownToRemark from 'remark-parse';
|
||||
import remarkToMarkdown from 'remark-stringify';
|
||||
|
||||
import remarkPaddedLinks from '../remarkPaddedLinks';
|
||||
|
||||
function input(markdown) {
|
||||
return unified()
|
||||
.use(markdownToRemark)
|
||||
.use(remarkPaddedLinks)
|
||||
.use(remarkToMarkdown)
|
||||
.processSync(markdown).contents;
|
||||
}
|
||||
|
||||
function output(markdown) {
|
||||
return unified().use(markdownToRemark).use(remarkToMarkdown).processSync(markdown).contents;
|
||||
}
|
||||
|
||||
describe('remarkPaddedLinks', () => {
|
||||
it('should move leading and trailing spaces outside of a link', () => {
|
||||
expect(input('[ a ](b)')).toEqual(output(' [a](b) '));
|
||||
});
|
||||
|
||||
it('should convert multiple leading or trailing spaces to a single space', () => {
|
||||
expect(input('[ a ](b)')).toEqual(output(' [a](b) '));
|
||||
});
|
||||
|
||||
it('should work with only a leading space or only a trailing space', () => {
|
||||
expect(input('[ a](b)[c ](d)')).toEqual(output(' [a](b)[c](d) '));
|
||||
});
|
||||
|
||||
it('should work for nested links', () => {
|
||||
expect(input('* # a[ b ](c)d')).toEqual(output('* # a [b](c) d'));
|
||||
});
|
||||
|
||||
it('should work for parents with multiple links that are not siblings', () => {
|
||||
expect(input('# a[ b ](c)d **[ e ](f)**')).toEqual(output('# a [b](c) d ** [e](f) **'));
|
||||
});
|
||||
|
||||
it('should work for links with arbitrarily nested children', () => {
|
||||
expect(input('[ a __*b*__ _c_ ](d)')).toEqual(output(' [a __*b*__ _c_](d) '));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,299 @@
|
||||
import visit from 'unist-util-visit';
|
||||
|
||||
import { markdownToRemark, remarkToMarkdown } from '..';
|
||||
|
||||
describe('registered remark plugins', () => {
|
||||
function withNetlifyLinks() {
|
||||
return function transformer(tree) {
|
||||
visit(tree, 'link', function onLink(node) {
|
||||
node.url = 'https://netlify.com';
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
it('should use remark transformer plugins when converting mdast to markdown', () => {
|
||||
const plugins = [withNetlifyLinks];
|
||||
const result = remarkToMarkdown(
|
||||
{
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: 'Some ',
|
||||
},
|
||||
{
|
||||
type: 'emphasis',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: 'important',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
value: ' text with ',
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
title: null,
|
||||
url: 'https://this-value-should-be-replaced.com',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: 'a link',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
value: ' in it.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(
|
||||
`"Some *important* text with [a link](https://netlify.com) in it."`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should use remark transformer plugins when converting markdown to mdast', () => {
|
||||
const plugins = [withNetlifyLinks];
|
||||
const result = markdownToRemark(
|
||||
'Some text with [a link](https://this-value-should-be-replaced.com) in it.',
|
||||
plugins,
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"position": Position {
|
||||
"end": Object {
|
||||
"column": 16,
|
||||
"line": 1,
|
||||
"offset": 15,
|
||||
},
|
||||
"indent": Array [],
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 1,
|
||||
"offset": 0,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "Some text with ",
|
||||
},
|
||||
Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"children": Array [],
|
||||
"position": Position {
|
||||
"end": Object {
|
||||
"column": 23,
|
||||
"line": 1,
|
||||
"offset": 22,
|
||||
},
|
||||
"indent": Array [],
|
||||
"start": Object {
|
||||
"column": 17,
|
||||
"line": 1,
|
||||
"offset": 16,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": "a link",
|
||||
},
|
||||
],
|
||||
"position": Position {
|
||||
"end": Object {
|
||||
"column": 67,
|
||||
"line": 1,
|
||||
"offset": 66,
|
||||
},
|
||||
"indent": Array [],
|
||||
"start": Object {
|
||||
"column": 16,
|
||||
"line": 1,
|
||||
"offset": 15,
|
||||
},
|
||||
},
|
||||
"title": null,
|
||||
"type": "link",
|
||||
"url": "https://netlify.com",
|
||||
},
|
||||
Object {
|
||||
"children": Array [],
|
||||
"position": Position {
|
||||
"end": Object {
|
||||
"column": 74,
|
||||
"line": 1,
|
||||
"offset": 73,
|
||||
},
|
||||
"indent": Array [],
|
||||
"start": Object {
|
||||
"column": 67,
|
||||
"line": 1,
|
||||
"offset": 66,
|
||||
},
|
||||
},
|
||||
"type": "text",
|
||||
"value": " in it.",
|
||||
},
|
||||
],
|
||||
"position": Position {
|
||||
"end": Object {
|
||||
"column": 74,
|
||||
"line": 1,
|
||||
"offset": 73,
|
||||
},
|
||||
"indent": Array [],
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 1,
|
||||
"offset": 0,
|
||||
},
|
||||
},
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"position": Object {
|
||||
"end": Object {
|
||||
"column": 74,
|
||||
"line": 1,
|
||||
"offset": 73,
|
||||
},
|
||||
"start": Object {
|
||||
"column": 1,
|
||||
"line": 1,
|
||||
"offset": 0,
|
||||
},
|
||||
},
|
||||
"type": "root",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should use remark serializer plugins when converting mdast to markdown', () => {
|
||||
function withEscapedLessThanChar() {
|
||||
if (this.Compiler) {
|
||||
this.Compiler.prototype.visitors.text = node => {
|
||||
return node.value.replace(/</g, '<');
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const plugins = [withEscapedLessThanChar];
|
||||
const result = remarkToMarkdown(
|
||||
{
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: '<3 Netlify',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`"<3 Netlify"`);
|
||||
});
|
||||
|
||||
it('should use remark preset with settings when converting mdast to markdown', () => {
|
||||
const settings = {
|
||||
emphasis: '_',
|
||||
bullet: '-',
|
||||
};
|
||||
|
||||
const plugins = [{ settings }];
|
||||
const result = remarkToMarkdown(
|
||||
{
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: 'Some ',
|
||||
},
|
||||
{
|
||||
type: 'emphasis',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: 'important',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
value: ' points:',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
ordered: false,
|
||||
start: null,
|
||||
spread: false,
|
||||
children: [
|
||||
{
|
||||
type: 'listItem',
|
||||
spread: false,
|
||||
checked: null,
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: 'One',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'listItem',
|
||||
spread: false,
|
||||
checked: null,
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: 'Two',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins,
|
||||
);
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
"Some _important_ points:
|
||||
|
||||
- One
|
||||
- Two"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
import { Map, OrderedMap } from 'immutable';
|
||||
import unified from 'unified';
|
||||
import markdownToRemarkPlugin from 'remark-parse';
|
||||
|
||||
import { remarkParseShortcodes, getLinesWithOffsets } from '../remarkShortcodes';
|
||||
|
||||
function process(value, plugins) {
|
||||
return unified()
|
||||
.use(markdownToRemarkPlugin, { fences: true, commonmark: true })
|
||||
.use(remarkParseShortcodes, { plugins })
|
||||
.parse(value);
|
||||
}
|
||||
|
||||
function EditorComponent({ id = 'foo', fromBlock = jest.fn(), pattern }) {
|
||||
return {
|
||||
id,
|
||||
fromBlock,
|
||||
pattern,
|
||||
};
|
||||
}
|
||||
|
||||
describe('remarkParseShortcodes', () => {
|
||||
describe('pattern matching', () => {
|
||||
it('should match multiline shortcodes', () => {
|
||||
const editorComponent = EditorComponent({ pattern: /^foo\nbar$/ });
|
||||
process('foo\nbar', Map({ [editorComponent.id]: editorComponent }));
|
||||
expect(editorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['foo\nbar']));
|
||||
});
|
||||
it('should match multiline shortcodes with empty lines', () => {
|
||||
const editorComponent = EditorComponent({ pattern: /^foo\n\nbar$/ });
|
||||
process('foo\n\nbar', Map({ [editorComponent.id]: editorComponent }));
|
||||
expect(editorComponent.fromBlock).toHaveBeenCalledWith(
|
||||
expect.arrayContaining(['foo\n\nbar']),
|
||||
);
|
||||
});
|
||||
it('should match shortcodes based on order of occurrence in value', () => {
|
||||
const fooEditorComponent = EditorComponent({ id: 'foo', pattern: /foo/ });
|
||||
const barEditorComponent = EditorComponent({ id: 'bar', pattern: /bar/ });
|
||||
process(
|
||||
'foo\n\nbar',
|
||||
OrderedMap([
|
||||
[barEditorComponent.id, barEditorComponent],
|
||||
[fooEditorComponent.id, fooEditorComponent],
|
||||
]),
|
||||
);
|
||||
expect(fooEditorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['foo']));
|
||||
});
|
||||
it('should match shortcodes based on order of occurrence in value even when some use line anchors', () => {
|
||||
const barEditorComponent = EditorComponent({ id: 'bar', pattern: /bar/ });
|
||||
const bazEditorComponent = EditorComponent({ id: 'baz', pattern: /^baz$/ });
|
||||
process(
|
||||
'foo\n\nbar\n\nbaz',
|
||||
OrderedMap([
|
||||
[bazEditorComponent.id, bazEditorComponent],
|
||||
[barEditorComponent.id, barEditorComponent],
|
||||
]),
|
||||
);
|
||||
expect(barEditorComponent.fromBlock).toHaveBeenCalledWith(expect.arrayContaining(['bar']));
|
||||
});
|
||||
});
|
||||
describe('parse', () => {
|
||||
describe('pattern with leading caret', () => {
|
||||
it('should be a remark shortcode node', () => {
|
||||
const editorComponent = EditorComponent({
|
||||
pattern: /^foo (?<bar>.+)$/,
|
||||
fromBlock: ({ groups }) => ({ bar: groups.bar }),
|
||||
});
|
||||
const mdast = process('foo baz', Map({ [editorComponent.id]: editorComponent }));
|
||||
expect(removePositions(mdast)).toMatchSnapshot();
|
||||
});
|
||||
it('should parse multiple shortcodes', () => {
|
||||
const editorComponent = EditorComponent({
|
||||
pattern: /foo (?<bar>.+)/,
|
||||
fromBlock: ({ groups }) => ({ bar: groups.bar }),
|
||||
});
|
||||
const mdast = process(
|
||||
'paragraph\n\nfoo bar\n\nfoo baz\n\nnext para',
|
||||
Map({ [editorComponent.id]: editorComponent }),
|
||||
);
|
||||
expect(removePositions(mdast)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('pattern without leading caret', () => {
|
||||
it('should handle pattern without leading caret', () => {
|
||||
const editorComponent = EditorComponent({
|
||||
pattern: /foo (?<bar>.+)/,
|
||||
fromBlock: ({ groups }) => ({ bar: groups.bar }),
|
||||
});
|
||||
const mdast = process(
|
||||
'paragraph\n\nfoo baz',
|
||||
Map({ [editorComponent.id]: editorComponent }),
|
||||
);
|
||||
expect(removePositions(mdast)).toMatchSnapshot();
|
||||
});
|
||||
it('should parse multiple shortcodes', () => {
|
||||
const editorComponent = EditorComponent({
|
||||
pattern: /foo (?<bar>.+)/,
|
||||
fromBlock: ({ groups }) => ({ bar: groups.bar }),
|
||||
});
|
||||
const mdast = process(
|
||||
'paragraph\n\nfoo bar\n\nfoo baz\n\nnext para',
|
||||
Map({ [editorComponent.id]: editorComponent }),
|
||||
);
|
||||
expect(removePositions(mdast)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function removePositions(obj) {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(removePositions);
|
||||
}
|
||||
if (obj && typeof obj === 'object') {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { position, ...rest } = obj;
|
||||
const result = {};
|
||||
for (const key in rest) {
|
||||
result[key] = removePositions(rest[key]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
});
|
||||
|
||||
describe('getLinesWithOffsets', () => {
|
||||
test('should split into lines', () => {
|
||||
const value = ' line1\n\nline2 \n\n line3 \n\n';
|
||||
|
||||
const lines = getLinesWithOffsets(value);
|
||||
expect(lines).toEqual([
|
||||
{ line: ' line1', start: 0 },
|
||||
{ line: 'line2', start: 8 },
|
||||
{ line: ' line3', start: 16 },
|
||||
{ line: '', start: 30 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('should return single item on no match', () => {
|
||||
const value = ' line1 ';
|
||||
|
||||
const lines = getLinesWithOffsets(value);
|
||||
expect(lines).toEqual([{ line: ' line1', start: 0 }]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { mergeAdjacentTexts } from '../remarkSlate';
|
||||
describe('remarkSlate', () => {
|
||||
describe('mergeAdjacentTexts', () => {
|
||||
it('should handle empty array', () => {
|
||||
const children = [];
|
||||
expect(mergeAdjacentTexts(children)).toBe(children);
|
||||
});
|
||||
|
||||
it('should merge adjacent texts with same marks', () => {
|
||||
const children = [
|
||||
{ text: '<a href="https://www.netlify.com" target="_blank">', marks: [] },
|
||||
{ text: 'Netlify', marks: [] },
|
||||
{ text: '</a>', marks: [] },
|
||||
];
|
||||
|
||||
expect(mergeAdjacentTexts(children)).toEqual([
|
||||
{
|
||||
text: '<a href="https://www.netlify.com" target="_blank">Netlify</a>',
|
||||
marks: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not merge adjacent texts with different marks', () => {
|
||||
const children = [
|
||||
{ text: '<a href="https://www.netlify.com" target="_blank">', marks: [] },
|
||||
{ text: 'Netlify', marks: ['b'] },
|
||||
{ text: '</a>', marks: [] },
|
||||
];
|
||||
|
||||
expect(mergeAdjacentTexts(children)).toEqual(children);
|
||||
});
|
||||
|
||||
it('should handle mixed children array', () => {
|
||||
const children = [
|
||||
{ object: 'inline' },
|
||||
{ text: '<a href="https://www.netlify.com" target="_blank">', marks: [] },
|
||||
{ text: 'Netlify', marks: [] },
|
||||
{ text: '</a>', marks: [] },
|
||||
{ object: 'inline' },
|
||||
{ text: '<a href="https://www.netlify.com" target="_blank">', marks: [] },
|
||||
{ text: 'Netlify', marks: ['b'] },
|
||||
{ text: '</a>', marks: [] },
|
||||
{ text: '<a href="https://www.netlify.com" target="_blank">', marks: [] },
|
||||
{ object: 'inline' },
|
||||
{ text: '</a>', marks: [] },
|
||||
];
|
||||
|
||||
expect(mergeAdjacentTexts(children)).toEqual([
|
||||
{ object: 'inline' },
|
||||
{
|
||||
text: '<a href="https://www.netlify.com" target="_blank">Netlify</a>',
|
||||
marks: [],
|
||||
},
|
||||
{ object: 'inline' },
|
||||
{ text: '<a href="https://www.netlify.com" target="_blank">', marks: [] },
|
||||
{ text: 'Netlify', marks: ['b'] },
|
||||
{
|
||||
text: '</a><a href="https://www.netlify.com" target="_blank">',
|
||||
marks: [],
|
||||
},
|
||||
{ object: 'inline' },
|
||||
{ text: '</a>', marks: [] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import unified from 'unified';
|
||||
import u from 'unist-builder';
|
||||
|
||||
import remarkStripTrailingBreaks from '../remarkStripTrailingBreaks';
|
||||
|
||||
function process(children) {
|
||||
const tree = u('root', children);
|
||||
const strippedMdast = unified().use(remarkStripTrailingBreaks).runSync(tree);
|
||||
|
||||
return strippedMdast.children;
|
||||
}
|
||||
|
||||
describe('remarkStripTrailingBreaks', () => {
|
||||
it('should remove trailing breaks at the end of a block', () => {
|
||||
expect(process([u('break')])).toEqual([]);
|
||||
expect(process([u('break'), u('text', '\n \n')])).toEqual([u('text', '\n \n')]);
|
||||
expect(process([u('text', 'a'), u('break')])).toEqual([u('text', 'a')]);
|
||||
});
|
||||
|
||||
it('should not remove trailing breaks that are not at the end of a block', () => {
|
||||
expect(process([u('break'), u('text', 'a')])).toEqual([u('break'), u('text', 'a')]);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user