mirror of
https://github.com/yuin/goldmark
synced 2025-03-04 23:04:52 +00:00
Compare commits
380 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04410ff159 | ||
|
|
f7b2d24c09 | ||
|
|
ba49c5c69d | ||
|
|
c05fb087a4 | ||
|
|
b39daae79e | ||
|
|
d9c03f07f0 | ||
|
|
65dcf6cd0a | ||
|
|
ad1565131a | ||
|
|
bc993b4f59 | ||
|
|
41273a4d07 | ||
|
|
d80ac9397c | ||
|
|
15000ac6a1 | ||
|
|
14d91f957f | ||
|
|
3847ca20c6 | ||
|
|
697e44ce88 | ||
|
|
fa88006eee | ||
|
|
fe34ea5d96 | ||
|
|
fd14edc9bc | ||
|
|
e367755421 | ||
|
|
e44645afbb | ||
|
|
15ade8aace | ||
|
|
dc32f35808 | ||
|
|
25bdeb0fee | ||
|
|
a590622b15 | ||
|
|
fde4948b4d | ||
|
|
9c09ae0019 | ||
|
|
c15e394c27 | ||
|
|
e405d57be0 | ||
|
|
ce6424aa0e | ||
|
|
09afa2feba | ||
|
|
4f3074451e | ||
|
|
4675c66d3d | ||
|
|
848dc66530 | ||
|
|
b8d6d3a9b7 | ||
|
|
90c46e0829 | ||
|
|
4bade05173 | ||
|
|
2b845f2615 | ||
|
|
e3d8ed9725 | ||
|
|
697cd509b1 | ||
|
|
ff3285aa2a | ||
|
|
c2167685c1 | ||
|
|
39a50c623e | ||
|
|
9c9003363f | ||
|
|
a89ad04c49 | ||
|
|
6b3067e7e7 | ||
|
|
6442ae1259 | ||
|
|
68e53654f2 | ||
|
|
04d4dd50ab | ||
|
|
8c6830d73b | ||
|
|
792af6819e | ||
|
|
9d0b1b6bb8 | ||
|
|
dc2230c235 | ||
|
|
2367b9ff46 | ||
|
|
6cbcfebb71 | ||
|
|
6ef9b10a3a | ||
|
|
d39ab8f93e | ||
|
|
9b02182dd0 | ||
|
|
ac56543632 | ||
|
|
2f1b40d881 | ||
|
|
254b9f8f77 | ||
|
|
31ccfc4039 | ||
|
|
b2df67847e | ||
|
|
4536092b45 | ||
|
|
2ce4086884 | ||
|
|
d9067d2324 | ||
|
|
69a0811de5 | ||
|
|
e46b1b5305 | ||
|
|
8e2127faa4 | ||
|
|
c6f0e7e0c5 | ||
|
|
d47eaf8c2d | ||
|
|
078e8aa5bb | ||
|
|
42f759fbe6 | ||
|
|
9d6f314b99 | ||
|
|
3505399723 | ||
|
|
82ff890073 | ||
|
|
550b0c7ca0 | ||
|
|
c446c414ef | ||
|
|
5e78751e90 | ||
|
|
023c1d9d21 | ||
|
|
60df4aadee | ||
|
|
a0962b569b | ||
|
|
a87c5778f9 | ||
|
|
b1ef69a870 | ||
|
|
aaeb9851a6 | ||
|
|
34756f289f | ||
|
|
c71a97b837 | ||
|
|
ae42b9179f | ||
|
|
1dd67d5750 | ||
|
|
95efaa1805 | ||
|
|
a3630e3073 | ||
|
|
3923ba0148 | ||
|
|
20df1691ad | ||
|
|
175c5ecd0c | ||
|
|
d77f38c53d | ||
|
|
42509ee585 | ||
|
|
db82c79f20 | ||
|
|
1efd8073c5 | ||
|
|
c0856327b3 | ||
|
|
8c219a7562 | ||
|
|
41e371a2d6 | ||
|
|
113ae87dd9 | ||
|
|
fb6057bf26 | ||
|
|
6bda32624d | ||
|
|
6bdcc0f927 | ||
|
|
fc877ab357 | ||
|
|
e29c1a5dfa | ||
|
|
96e98dca10 | ||
|
|
84ce6bc8ba | ||
|
|
f28136bf1c | ||
|
|
6d6707526e | ||
|
|
31179abc2d | ||
|
|
2184cbf49b | ||
|
|
67340c7d10 | ||
|
|
5ffbecc0c5 | ||
|
|
8da81dfae2 | ||
|
|
3563ceb58e | ||
|
|
f04f7f5340 | ||
|
|
e64a68fc13 | ||
|
|
0af271269f | ||
|
|
a816d4652e | ||
|
|
920c3818d4 | ||
|
|
be2bf82af9 | ||
|
|
5ba3327fda | ||
|
|
1def545b06 | ||
|
|
7aa0ead60c | ||
|
|
5e24a62400 | ||
|
|
a6c48071ed | ||
|
|
7b616a4c80 | ||
|
|
c3b7691431 | ||
|
|
b7b0919dfe | ||
|
|
41b1d4542d | ||
|
|
f6e93ffd8f | ||
|
|
d44b18596f | ||
|
|
333aa4d779 | ||
|
|
907eb99835 | ||
|
|
0283c9c543 | ||
|
|
4477be72ee | ||
|
|
b2c88c80f6 | ||
|
|
661ccb7c9e | ||
|
|
7557842636 | ||
|
|
15ea97611d | ||
|
|
4b793a1aed | ||
|
|
324b2d6e6f | ||
|
|
829dc0ae24 | ||
|
|
e77ca9231a | ||
|
|
05d89a0b45 | ||
|
|
beafde4b8f | ||
|
|
cbaee30aee | ||
|
|
20a276ea45 | ||
|
|
bc90544cef | ||
|
|
f5dcbd8208 | ||
|
|
2f8abf5949 | ||
|
|
829d874034 | ||
|
|
d44652d174 | ||
|
|
97df31c2ec | ||
|
|
a8ed3c4205 | ||
|
|
457c157ed5 | ||
|
|
4317d98509 | ||
|
|
1306649a65 | ||
|
|
7efc483c26 | ||
|
|
351308fb72 | ||
|
|
f37563cfa8 | ||
|
|
466482bf19 | ||
|
|
fad80b4f0c | ||
|
|
8174177880 | ||
|
|
aae0486a04 | ||
|
|
1d522188b9 | ||
|
|
5588d92a56 | ||
|
|
759cc35c3a | ||
|
|
040b478cdb | ||
|
|
38f7fc92ff | ||
|
|
43353aeea4 | ||
|
|
16e27ac471 | ||
|
|
9568b57c4b | ||
|
|
155754ef6e | ||
|
|
ab9c801417 | ||
|
|
db22606847 | ||
|
|
9f5125e104 | ||
|
|
baec0941d2 | ||
|
|
cd69b03a4e | ||
|
|
24d1a350c2 | ||
|
|
ab798ea4aa | ||
|
|
304f513394 | ||
|
|
3fcf875f9f | ||
|
|
017596e61e | ||
|
|
75d8cce5b7 | ||
|
|
2913ca2902 | ||
|
|
c53c1a4cfe | ||
|
|
56bbdf0370 | ||
|
|
2ffadcefcf | ||
|
|
f3e20f4795 | ||
|
|
3d7ce16f2f | ||
|
|
c4b3054802 | ||
|
|
4422fe4f7b | ||
|
|
3549ad5b05 | ||
|
|
e7353e6299 | ||
|
|
6c741ae251 | ||
|
|
748fc079dc | ||
|
|
033db7dd82 | ||
|
|
4e0c22a20e | ||
|
|
d64a5a61f8 | ||
|
|
036c8738df | ||
|
|
5e417f871d | ||
|
|
fa0cf89934 | ||
|
|
af880df797 | ||
|
|
ccd1cd6819 | ||
|
|
9e0189df27 | ||
|
|
da9729d134 | ||
|
|
6de424ee3b | ||
|
|
7b90f04af4 | ||
|
|
d102aef53c | ||
|
|
e880f8545b | ||
|
|
91e5269fb0 | ||
|
|
bd58441cc1 | ||
|
|
b7f25d6cd9 | ||
|
|
b91c802b8c | ||
|
|
b24f9b4dd7 | ||
|
|
3c3d4481ef | ||
|
|
feff0bb82b | ||
|
|
e331790a2b | ||
|
|
33089e232f | ||
|
|
4a49786b16 | ||
|
|
6eb3819f4b | ||
|
|
2141537561 | ||
|
|
813d953eeb | ||
|
|
3d78558cf2 | ||
|
|
4709d43b81 | ||
|
|
3393022ab6 | ||
|
|
a302193b06 | ||
|
|
184f8ef622 | ||
|
|
1f967da77c | ||
|
|
66874b397f | ||
|
|
54b1e988cc | ||
|
|
12fc98ebcd | ||
|
|
84d07d567d | ||
|
|
785b85a76a | ||
|
|
ff84cd3a51 | ||
|
|
2bfdf48a98 | ||
|
|
fea52e86ab | ||
|
|
5ab7c64e28 | ||
|
|
5c877c8afe | ||
|
|
21437947a3 | ||
|
|
a727b5adb2 | ||
|
|
4a200405d7 | ||
|
|
3a828e641d | ||
|
|
2a86c1ea31 | ||
|
|
70a5404a11 | ||
|
|
8bdab9449a | ||
|
|
0b49730177 | ||
|
|
3c340e9970 | ||
|
|
39db45a099 | ||
|
|
faaafa55b6 | ||
|
|
45fa0e2645 | ||
|
|
5294fa5c4f | ||
|
|
4fc27178e4 | ||
|
|
3e38e966f6 | ||
|
|
97405beafd | ||
|
|
4fedb553e3 | ||
|
|
4f96330d3f | ||
|
|
533a448c5f | ||
|
|
94947895e7 | ||
|
|
83e7dcebc7 | ||
|
|
4b54582dee | ||
|
|
471456b80a | ||
|
|
f9b134f6be | ||
|
|
2c9db0c8fa | ||
|
|
66a48f66b8 | ||
|
|
224bf7d721 | ||
|
|
9a4c7bce92 | ||
|
|
923eb97048 | ||
|
|
e4108d55a4 | ||
|
|
12851a08ba | ||
|
|
efd5960110 | ||
|
|
a6724f003f | ||
|
|
e0a5ff07c2 | ||
|
|
5690da2615 | ||
|
|
2aab93edb4 | ||
|
|
e0171097bf | ||
|
|
26fb0b56e6 | ||
|
|
64d4e16bf4 | ||
|
|
6d2e5fddae | ||
|
|
5b25bb53dd | ||
|
|
26b4b47473 | ||
|
|
9e55545c8d | ||
|
|
ec246695c5 | ||
|
|
7d8bee11ca | ||
|
|
0a62f6ae63 | ||
|
|
a47a029d55 | ||
|
|
e528cffacf | ||
|
|
89919744e0 | ||
|
|
171dbc66a8 | ||
|
|
ea2c6c34ce | ||
|
|
66aa5562f7 | ||
|
|
43e0347f6d | ||
|
|
25f82f0a2d | ||
|
|
30605bf736 | ||
|
|
567046a85d | ||
|
|
6f9629fb2b | ||
|
|
511e434efc | ||
|
|
8a50115f03 | ||
|
|
5334c63923 | ||
|
|
ac8e225cd3 | ||
|
|
68dcec6ac4 | ||
|
|
6efe809cde | ||
|
|
eb2667632a | ||
|
|
615d5706c6 | ||
|
|
2f292e5b74 | ||
|
|
74e1374f5a | ||
|
|
ff066ede82 | ||
|
|
3dc5ebdb17 | ||
|
|
2932dadfb3 | ||
|
|
748be0c096 | ||
|
|
9f9f8f0e5e | ||
|
|
8549b83b0c | ||
|
|
54fc7c3f18 | ||
|
|
b611cd333a | ||
|
|
6c55ba55a1 | ||
|
|
4536e57938 | ||
|
|
fba5de7344 | ||
|
|
9dec7e9e8b | ||
|
|
c8c3b41fe0 | ||
|
|
c999f5a9a7 | ||
|
|
22bbda3653 | ||
|
|
76006af024 | ||
|
|
d51543d817 | ||
|
|
66a1061d96 | ||
|
|
8aefee4a22 | ||
|
|
696c860a32 | ||
|
|
afc3654ecf | ||
|
|
ea8789f650 | ||
|
|
16b69522a4 | ||
|
|
2184586bb2 | ||
|
|
13a98719d4 | ||
|
|
187643a437 | ||
|
|
667a2920f2 | ||
|
|
4a770685c0 | ||
|
|
8c55e6fa9c | ||
|
|
f98eb987aa | ||
|
|
d7e925c896 | ||
|
|
45376ddb05 | ||
|
|
b067a12f6b | ||
|
|
3190eb8348 | ||
|
|
e7035b1993 | ||
|
|
008c258471 | ||
|
|
0c44174564 | ||
|
|
7950956e28 | ||
|
|
6f6884271d | ||
|
|
d29104889a | ||
|
|
17a47ea1e7 | ||
|
|
a27b0ef209 | ||
|
|
21b4a046d0 | ||
|
|
19b18e85fc | ||
|
|
883918a85c | ||
|
|
36e42c4e73 | ||
|
|
6369ad43e8 | ||
|
|
032792ae55 | ||
|
|
b7348f0573 | ||
|
|
0fc2859377 | ||
|
|
57c4df05d5 | ||
|
|
dd1590372c | ||
|
|
8aee2d32fd | ||
|
|
23ace7da03 | ||
|
|
05645dd3c4 | ||
|
|
7b1dd7b221 | ||
|
|
9b1570bcde | ||
|
|
9cf9f6802e | ||
|
|
ddce9bf23e | ||
|
|
3802559573 | ||
|
|
8df88b3d75 | ||
|
|
4586ec683b | ||
|
|
f95910174c | ||
|
|
2ddc99baff | ||
|
|
7d92c09d44 | ||
|
|
1963434c50 | ||
|
|
e481813300 | ||
|
|
6703518300 | ||
|
|
e59897f8e5 | ||
|
|
4febcc9861 | ||
|
|
4ff89123a4 | ||
|
|
31fd0f6b4c |
95 changed files with 16952 additions and 7261 deletions
17
.github/ISSUE_TEMPLATE.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE.md
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
goldmark has [https://github.com/yuin/goldmark/discussions](Discussions) in github.
|
||||
You should post only issues here. Feature requests and questions should be posted at discussions.
|
||||
|
||||
|
||||
- [ ] goldmark is fully compliant with the CommonMark. Before submitting issue, you **must** read [CommonMark spec](https://spec.commonmark.org/0.29/) and confirm your output is different from [CommonMark online demo](https://spec.commonmark.org/dingus/).
|
||||
- [ ] **Extensions(Autolink without `<` `>`, Table, etc) are not part of CommonMark spec.** You should confirm your output is different from other official renderers correspond with an extension.
|
||||
- [ ] **goldmark is not dedicated for Hugo**. If you are Hugo user and your issue was raised by your experience in Hugo, **you should consider create issue at Hugo repository at first** .
|
||||
|
||||
Please answer the following before submitting your issue:
|
||||
|
||||
1. What version of goldmark are you using? :
|
||||
2. What version of Go are you using? :
|
||||
3. What operating system and processor architecture are you using? :
|
||||
4. What did you do? :
|
||||
5. What did you expect to see? :
|
||||
6. What did you see instead? :
|
||||
7. Did you confirm your output is different from [CommonMark online demo](https://spec.commonmark.org/dingus/) or other official renderer correspond with an extension?:
|
||||
26
.github/workflows/stale.yaml
vendored
Normal file
26
.github/workflows/stale.yaml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
name: Close inactive issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 9 * * *"
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
days-before-issue-stale: 30
|
||||
days-before-issue-close: 14
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
|
||||
exempt-issue-labels: "pinned,security"
|
||||
days-before-pr-stale: 180
|
||||
days-before-pr-close: 14
|
||||
stale-pr-label: "stale"
|
||||
stale-pr-message: "This PR is stale because it has been open for 180 days with no activity."
|
||||
close-pr-message: "This PR was closed because it has been inactive for 14 days since being marked as stale."
|
||||
exempt-pr-labels: "pinned,security"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
33
.github/workflows/test.yaml
vendored
Normal file
33
.github/workflows/test.yaml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
on: [push, pull_request]
|
||||
name: test
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version: [1.21.x, 1.22.x]
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Run lints
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: latest
|
||||
if: "matrix.platform == 'ubuntu-latest'" # gofmt linter fails on Windows for CRLF problems
|
||||
- name: Run tests
|
||||
env:
|
||||
GOLDMARK_TEST_TIMEOUT_MULTIPLIER: 5
|
||||
run: go test -v ./... -covermode=count -coverprofile=coverage.out -coverpkg=./...
|
||||
- name: Install goveralls
|
||||
run: go install github.com/mattn/goveralls@latest
|
||||
- name: Send coverage
|
||||
if: "matrix.platform == 'ubuntu-latest'"
|
||||
env:
|
||||
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: goveralls -coverprofile=coverage.out -service=github
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -11,3 +11,9 @@
|
|||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
.DS_Store
|
||||
fuzz/corpus
|
||||
fuzz/crashers
|
||||
fuzz/suppressions
|
||||
fuzz/fuzz-fuzz.zip
|
||||
|
|
|
|||
105
.golangci.yml
Normal file
105
.golangci.yml
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
run:
|
||||
deadline: 10m
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
exclude-rules:
|
||||
- path: _test.go
|
||||
linters:
|
||||
- errcheck
|
||||
- lll
|
||||
exclude:
|
||||
- "Package util"
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- typecheck
|
||||
- unused
|
||||
- gofmt
|
||||
- godot
|
||||
- makezero
|
||||
- misspell
|
||||
- revive
|
||||
- wastedassign
|
||||
- lll
|
||||
|
||||
linters-settings:
|
||||
revive:
|
||||
severity: "warning"
|
||||
confidence: 0.8
|
||||
rules:
|
||||
- name: blank-imports
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: context-as-argument
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: context-keys-type
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: dot-imports
|
||||
severity: warning
|
||||
disabled: true
|
||||
- name: error-return
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: error-strings
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: error-naming
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: exported
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: increment-decrement
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: var-naming
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: var-declaration
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: package-comments
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: range
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: receiver-naming
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: time-naming
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: unexported-return
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: indent-error-flow
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: errorf
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: empty-block
|
||||
severity: warning
|
||||
disabled: true
|
||||
- name: superfluous-else
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: unused-parameter
|
||||
severity: warning
|
||||
disabled: true
|
||||
- name: unreachable-code
|
||||
severity: warning
|
||||
disabled: false
|
||||
- name: redefines-builtin-id
|
||||
severity: warning
|
||||
disabled: false
|
||||
17
.travis.yml
17
.travis.yml
|
|
@ -1,17 +0,0 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- "1.11.x"
|
||||
- "1.12.x"
|
||||
env:
|
||||
global:
|
||||
GO111MODULE=off
|
||||
|
||||
before_install:
|
||||
- go get github.com/axw/gocov/gocov
|
||||
- go get github.com/mattn/goveralls
|
||||
- if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
|
||||
install:
|
||||
- go get -u -v $(go list -f '{{join .Imports "\n"}}{{"\n"}}{{join .TestImports "\n"}}' ./... | sort | uniq | grep '\.' | grep -v goldmark)
|
||||
script:
|
||||
- $HOME/gopath/bin/goveralls -service=travis-ci
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 Yusuke Inuzuka
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
8
Makefile
8
Makefile
|
|
@ -1,7 +1,13 @@
|
|||
.PHONY: test
|
||||
.PHONY: test fuzz lint
|
||||
|
||||
lint:
|
||||
golangci-lint run -c .golangci.yml ./...
|
||||
|
||||
test:
|
||||
go test -coverprofile=profile.out -coverpkg=github.com/yuin/goldmark,github.com/yuin/goldmark/ast,github.com/yuin/goldmark/extension,github.com/yuin/goldmark/extension/ast,github.com/yuin/goldmark/parser,github.com/yuin/goldmark/renderer,github.com/yuin/goldmark/renderer/html,github.com/yuin/goldmark/text,github.com/yuin/goldmark/util ./...
|
||||
|
||||
cov: test
|
||||
go tool cover -html=profile.out
|
||||
|
||||
fuzz:
|
||||
cd ./fuzz && go test -fuzz=Fuzz
|
||||
|
|
|
|||
523
README.md
523
README.md
|
|
@ -1,58 +1,78 @@
|
|||
goldmark
|
||||
==========================================
|
||||
|
||||
[](http://godoc.org/github.com/yuin/goldmark)
|
||||
[](https://travis-ci.org/yuin/goldmark)
|
||||
[](https://coveralls.io/r/yuin/goldmark)
|
||||
[](https://pkg.go.dev/github.com/yuin/goldmark)
|
||||
[](https://github.com/yuin/goldmark/actions?query=workflow:test)
|
||||
[](https://coveralls.io/github/yuin/goldmark)
|
||||
[](https://goreportcard.com/report/github.com/yuin/goldmark)
|
||||
|
||||
> A markdown parser written in Go. Easy to extend, standard compliant, well structured.
|
||||
> A Markdown parser written in Go. Easy to extend, standards-compliant, well-structured.
|
||||
|
||||
goldmark is compliant to CommonMark 0.29.
|
||||
goldmark is compliant with CommonMark 0.31.2.
|
||||
|
||||
- [goldmark playground](https://yuin.github.io/goldmark/playground/) : Try goldmark online. This playground is built with WASM(5-10MB).
|
||||
|
||||
Motivation
|
||||
----------------------
|
||||
I need a markdown parser for Go that meets following conditions:
|
||||
I needed a Markdown parser for Go that satisfies the following requirements:
|
||||
|
||||
- Easy to extend.
|
||||
- Markdown is poor in document expressions compared with other light markup languages like restructuredText.
|
||||
- We have extended a markdown syntax. i.e. : PHPMarkdownExtra, Github Flavored Markdown.
|
||||
- Standard compliant.
|
||||
- Markdown is poor in document expressions compared to other light markup languages such as reStructuredText.
|
||||
- We have extensions to the Markdown syntax, e.g. PHP Markdown Extra, GitHub Flavored Markdown.
|
||||
- Standards-compliant.
|
||||
- Markdown has many dialects.
|
||||
- Github Flavored Markdown is widely used and it is based on CommonMark aside from whether CommonMark is good specification or not.
|
||||
- CommonMark is too complicated and hard to implement.
|
||||
- Well structured.
|
||||
- AST based, and preserves source potision of nodes.
|
||||
- GitHub-Flavored Markdown is widely used and is based upon CommonMark, effectively mooting the question of whether or not CommonMark is an ideal specification.
|
||||
- CommonMark is complicated and hard to implement.
|
||||
- Well-structured.
|
||||
- AST-based; preserves source position of nodes.
|
||||
- Written in pure Go.
|
||||
|
||||
[golang-commonmark](https://gitlab.com/golang-commonmark/markdown) may be a good choice, but it seems copy of the [markdown-it](https://github.com/markdown-it) .
|
||||
[golang-commonmark](https://gitlab.com/golang-commonmark/markdown) may be a good choice, but it seems to be a copy of [markdown-it](https://github.com/markdown-it).
|
||||
|
||||
[blackfriday.v2](https://github.com/russross/blackfriday/tree/v2) is a fast and widely used implementation, but it is not CommonMark compliant and can not be extended from outside of the package since it's AST is not interfaces but structs.
|
||||
[blackfriday.v2](https://github.com/russross/blackfriday/tree/v2) is a fast and widely-used implementation, but is not CommonMark-compliant and cannot be extended from outside of the package, since its AST uses structs instead of interfaces.
|
||||
|
||||
Furthermore, its behavior differs with other implementations in some cases especially of lists. ([Deep nested lists don't output correctly #329](https://github.com/russross/blackfriday/issues/329), [List block cannot have a second line #244](https://github.com/russross/blackfriday/issues/244), etc).
|
||||
Furthermore, its behavior differs from other implementations in some cases, especially regarding lists: [Deep nested lists don't output correctly #329](https://github.com/russross/blackfriday/issues/329), [List block cannot have a second line #244](https://github.com/russross/blackfriday/issues/244), etc.
|
||||
|
||||
This behavior sometimes causes problems. If you migrate your markdown text to blackfriday based wikis from Github, many lists will immediately be broken.
|
||||
This behavior sometimes causes problems. If you migrate your Markdown text from GitHub to blackfriday-based wikis, many lists will immediately be broken.
|
||||
|
||||
As mentioned above, CommonMark is too complicated and hard to implement, So Markdown parsers base on CommonMark barely exist.
|
||||
As mentioned above, CommonMark is complicated and hard to implement, so Markdown parsers based on CommonMark are few and far between.
|
||||
|
||||
Features
|
||||
----------------------
|
||||
|
||||
- **Standard compliant.** : goldmark get full compliance with latest CommonMark spec.
|
||||
- **Extensible.** : Do you want to add a `@username` mention syntax to the markdown?
|
||||
You can easily do it in goldmark. You can add your AST nodes,
|
||||
parsers for block level elements, parsers for inline level elements,
|
||||
transformers for paragraphs, transformers for whole AST structure, and
|
||||
- **Standards-compliant.** goldmark is fully compliant with the latest [CommonMark](https://commonmark.org/) specification.
|
||||
- **Extensible.** Do you want to add a `@username` mention syntax to Markdown?
|
||||
You can easily do so in goldmark. You can add your AST nodes,
|
||||
parsers for block-level elements, parsers for inline-level elements,
|
||||
transformers for paragraphs, transformers for the whole AST structure, and
|
||||
renderers.
|
||||
- **Preformance.** : goldmark performs pretty much equally to the cmark
|
||||
(CommonMark reference implementation written in c).
|
||||
- **Builtin extensions.** : goldmark ships with common extensions like tables, strikethrough,
|
||||
- **Performance.** goldmark's performance is on par with that of cmark,
|
||||
the CommonMark reference implementation written in C.
|
||||
- **Robust.** goldmark is tested with `go test --fuzz`.
|
||||
- **Built-in extensions.** goldmark ships with common extensions like tables, strikethrough,
|
||||
task lists, and definition lists.
|
||||
- **Depends only on standard libraries.**
|
||||
|
||||
Installation
|
||||
----------------------
|
||||
```bash
|
||||
$ go get github.com/yuin/goldmark
|
||||
```
|
||||
|
||||
|
||||
Usage
|
||||
----------------------
|
||||
Import packages:
|
||||
|
||||
Convert Markdown documents with the CommonMark compliant mode:
|
||||
```go
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/yuin/goldmark"
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
Convert Markdown documents with the CommonMark-compliant mode:
|
||||
|
||||
```go
|
||||
var buf bytes.Buffer
|
||||
|
|
@ -61,10 +81,40 @@ if err := goldmark.Convert(source, &buf); err != nil {
|
|||
}
|
||||
```
|
||||
|
||||
Customize a parser and a renderer:
|
||||
With options
|
||||
------------------------------
|
||||
|
||||
```go
|
||||
md := goldmark.NewMarkdown(
|
||||
var buf bytes.Buffer
|
||||
if err := goldmark.Convert(source, &buf, parser.WithContext(ctx)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
| Functional option | Type | Description |
|
||||
| ----------------- | ---- | ----------- |
|
||||
| `parser.WithContext` | A `parser.Context` | Context for the parsing phase. |
|
||||
|
||||
Context options
|
||||
----------------------
|
||||
|
||||
| Functional option | Type | Description |
|
||||
| ----------------- | ---- | ----------- |
|
||||
| `parser.WithIDs` | A `parser.IDs` | `IDs` allows you to change logics that are related to element id(ex: Auto heading id generation). |
|
||||
|
||||
|
||||
Custom parser and renderer
|
||||
--------------------------
|
||||
```go
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
)
|
||||
|
||||
md := goldmark.New(
|
||||
goldmark.WithExtensions(extension.GFM),
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAutoHeadingID(),
|
||||
|
|
@ -80,6 +130,14 @@ if err := md.Convert(source, &buf); err != nil {
|
|||
}
|
||||
```
|
||||
|
||||
| Functional option | Type | Description |
|
||||
| ----------------- | ---- | ----------- |
|
||||
| `goldmark.WithParser` | `parser.Parser` | This option must be passed before `goldmark.WithParserOptions` and `goldmark.WithExtensions` |
|
||||
| `goldmark.WithRenderer` | `renderer.Renderer` | This option must be passed before `goldmark.WithRendererOptions` and `goldmark.WithExtensions` |
|
||||
| `goldmark.WithParserOptions` | `...parser.Option` | |
|
||||
| `goldmark.WithRendererOptions` | `...renderer.Option` | |
|
||||
| `goldmark.WithExtensions` | `...goldmark.Extender` | |
|
||||
|
||||
Parser and Renderer options
|
||||
------------------------------
|
||||
|
||||
|
|
@ -87,51 +145,62 @@ Parser and Renderer options
|
|||
|
||||
| Functional option | Type | Description |
|
||||
| ----------------- | ---- | ----------- |
|
||||
| `parser.WithBlockParsers` | A `util.PrioritizedSlice` whose elements are `parser.BlockParser` | Parsers for parsing block level elements. |
|
||||
| `parser.WithInlineParsers` | A `util.PrioritizedSlice` whose elements are `parser.InlineParser` | Parsers for parsing inline level elements. |
|
||||
| `parser.WithParagraphTransformers` | A `util.PrioritizedSlice` whose elements are `parser.ParagraphTransformer` | Transformers for transforming paragraph nodes. |
|
||||
| `parser.WithBlockParsers` | A `util.PrioritizedSlice` whose elements are `parser.BlockParser` | Parsers for parsing block level elements. |
|
||||
| `parser.WithInlineParsers` | A `util.PrioritizedSlice` whose elements are `parser.InlineParser` | Parsers for parsing inline level elements. |
|
||||
| `parser.WithParagraphTransformers` | A `util.PrioritizedSlice` whose elements are `parser.ParagraphTransformer` | Transformers for transforming paragraph nodes. |
|
||||
| `parser.WithASTTransformers` | A `util.PrioritizedSlice` whose elements are `parser.ASTTransformer` | Transformers for transforming an AST. |
|
||||
| `parser.WithAutoHeadingID` | `-` | Enables auto heading ids. |
|
||||
| `parser.WithAttribute` | `-` | Enables custom attributes. Currently only headings supports attributes. |
|
||||
| `parser.WithFilterTags` | `...string` | HTML tag names forbidden in HTML blocks and Raw HTMLs. |
|
||||
|
||||
### HTML Renderer options
|
||||
|
||||
| Functional option | Type | Description |
|
||||
| ----------------- | ---- | ----------- |
|
||||
| `html.WithWriter` | `html.Writer` | `html.Writer` for writing contents to an `io.Writer`. |
|
||||
| `html.WithHardWraps` | `-` | Render new lines as `<br>`.|
|
||||
| `html.WithHardWraps` | `-` | Render newlines as `<br>`.|
|
||||
| `html.WithXHTML` | `-` | Render as XHTML. |
|
||||
| `html.WithUnsafe` | `-` | By default, goldmark does not render raw HTMLs and potentially dangerous links. With this option, goldmark renders these contents as it is. |
|
||||
| `html.WithUnsafe` | `-` | By default, goldmark does not render raw HTML or potentially dangerous links. With this option, goldmark renders such content as written. |
|
||||
|
||||
### Built-in extensions
|
||||
|
||||
- `extension.Table`
|
||||
- [Gitmark Flavored Markdown: Tables](https://github.github.com/gfm/#tables-extension-)
|
||||
- [GitHub Flavored Markdown: Tables](https://github.github.com/gfm/#tables-extension-)
|
||||
- `extension.Strikethrough`
|
||||
- [Gitmark Flavored Markdown: Strikethrough](https://github.github.com/gfm/#strikethrough-extension-)
|
||||
- [GitHub Flavored Markdown: Strikethrough](https://github.github.com/gfm/#strikethrough-extension-)
|
||||
- `extension.Linkify`
|
||||
- [Gitmark Flavored Markdown: Autolinks](https://github.github.com/gfm/#autolinks-extension-)
|
||||
- [GitHub Flavored Markdown: Autolinks](https://github.github.com/gfm/#autolinks-extension-)
|
||||
- `extension.TaskList`
|
||||
- [Gitmark Flavored Markdown: Task list items](https://github.github.com/gfm/#task-list-items-extension-)
|
||||
- [GitHub Flavored Markdown: Task list items](https://github.github.com/gfm/#task-list-items-extension-)
|
||||
- `extension.GFM`
|
||||
- This extension enables Table, Strikethrough, Linkify and TaskList.
|
||||
In addition, this extension sets some tags to `parser.FilterTags` .
|
||||
- This extension enables Table, Strikethrough, Linkify and TaskList.
|
||||
- This extension does not filter tags defined in [6.11: Disallowed Raw HTML (extension)](https://github.github.com/gfm/#disallowed-raw-html-extension-).
|
||||
If you need to filter HTML tags, see [Security](#security).
|
||||
- If you need to parse github emojis, you can use [goldmark-emoji](https://github.com/yuin/goldmark-emoji) extension.
|
||||
- `extension.DefinitionList`
|
||||
- [PHP Markdown Extra: Definition lists](https://michelf.ca/projects/php-markdown/extra/#def-list)
|
||||
- [PHP Markdown Extra: Definition lists](https://michelf.ca/projects/php-markdown/extra/#def-list)
|
||||
- `extension.Footnote`
|
||||
- [PHP Markdown Extra: Footnotes](https://michelf.ca/projects/php-markdown/extra/#footnotes)
|
||||
- [PHP Markdown Extra: Footnotes](https://michelf.ca/projects/php-markdown/extra/#footnotes)
|
||||
- `extension.Typographer`
|
||||
- This extension substitutes punctuations with typographic entities like [smartypants](https://daringfireball.net/projects/smartypants/).
|
||||
- This extension substitutes punctuations with typographic entities like [smartypants](https://daringfireball.net/projects/smartypants/).
|
||||
- `extension.CJK`
|
||||
- This extension is a shortcut for CJK related functionalities.
|
||||
|
||||
### Attributes
|
||||
`parser.WithAttribute` option allows you to define attributes on some elements.
|
||||
The `parser.WithAttribute` option allows you to define attributes on some elements.
|
||||
|
||||
Currently only headings support attributes.
|
||||
|
||||
**Attributes are being discussed in the
|
||||
[CommonMark forum](https://talk.commonmark.org/t/consistent-attribute-syntax/272).
|
||||
This syntax may possibly change in the future.**
|
||||
|
||||
|
||||
#### Headings
|
||||
|
||||
```
|
||||
## heading ## {#id .className attrName=attrValue class="class1 class2"}
|
||||
|
||||
## heading {#id .className attrName=attrValue class="class1 class2"}
|
||||
```
|
||||
|
||||
```
|
||||
|
|
@ -139,13 +208,25 @@ heading {#id .className attrName=attrValue}
|
|||
============
|
||||
```
|
||||
|
||||
### Table extension
|
||||
The Table extension implements [Table(extension)](https://github.github.com/gfm/#tables-extension-), as
|
||||
defined in [GitHub Flavored Markdown Spec](https://github.github.com/gfm/).
|
||||
|
||||
Specs are defined for XHTML, so specs use some deprecated attributes for HTML5.
|
||||
|
||||
You can override alignment rendering method via options.
|
||||
|
||||
| Functional option | Type | Description |
|
||||
| ----------------- | ---- | ----------- |
|
||||
| `extension.WithTableCellAlignMethod` | `extension.TableCellAlignMethod` | Option indicates how are table cells aligned. |
|
||||
|
||||
### Typographer extension
|
||||
|
||||
Typographer extension translates plain ASCII punctuation characters into typographic punctuation HTML entities.
|
||||
The Typographer extension translates plain ASCII punctuation characters into typographic-punctuation HTML entities.
|
||||
|
||||
Default substitutions are:
|
||||
|
||||
| Punctuation | Default entitiy |
|
||||
| Punctuation | Default entity |
|
||||
| ------------ | ---------- |
|
||||
| `'` | `‘`, `’` |
|
||||
| `"` | `“`, `”` |
|
||||
|
|
@ -155,25 +236,313 @@ Default substitutions are:
|
|||
| `<<` | `«` |
|
||||
| `>>` | `»` |
|
||||
|
||||
You can overwrite the substitutions by `extensions.WithTypographicSubstitutions`.
|
||||
You can override the default substitutions via `extensions.WithTypographicSubstitutions`:
|
||||
|
||||
```go
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
extension.NewTypographer(
|
||||
extension.WithTypographicSubstitutions(extension.TypographicSubstitutions{
|
||||
extension.LeftSingleQuote: []byte("‚"),
|
||||
extension.RightSingleQuote: nil, // nil disables a substitution
|
||||
}),
|
||||
),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
extension.NewTypographer(
|
||||
extension.WithTypographicSubstitutions(extension.TypographicSubstitutions{
|
||||
extension.LeftSingleQuote: []byte("‚"),
|
||||
extension.RightSingleQuote: nil, // nil disables a substitution
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Linkify extension
|
||||
|
||||
The Linkify extension implements [Autolinks(extension)](https://github.github.com/gfm/#autolinks-extension-), as
|
||||
defined in [GitHub Flavored Markdown Spec](https://github.github.com/gfm/).
|
||||
|
||||
Since the spec does not define details about URLs, there are numerous ambiguous cases.
|
||||
|
||||
You can override autolinking patterns via options.
|
||||
|
||||
| Functional option | Type | Description |
|
||||
| ----------------- | ---- | ----------- |
|
||||
| `extension.WithLinkifyAllowedProtocols` | `[][]byte \| []string` | List of allowed protocols such as `[]string{ "http:" }` |
|
||||
| `extension.WithLinkifyURLRegexp` | `*regexp.Regexp` | Regexp that defines URLs, including protocols |
|
||||
| `extension.WithLinkifyWWWRegexp` | `*regexp.Regexp` | Regexp that defines URL starting with `www.`. This pattern corresponds to [the extended www autolink](https://github.github.com/gfm/#extended-www-autolink) |
|
||||
| `extension.WithLinkifyEmailRegexp` | `*regexp.Regexp` | Regexp that defines email addresses` |
|
||||
|
||||
Example, using [xurls](https://github.com/mvdan/xurls):
|
||||
|
||||
```go
|
||||
import "mvdan.cc/xurls/v2"
|
||||
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
extension.NewLinkify(
|
||||
extension.WithLinkifyAllowedProtocols([]string{
|
||||
"http:",
|
||||
"https:",
|
||||
}),
|
||||
extension.WithLinkifyURLRegexp(
|
||||
xurls.Strict(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Footnotes extension
|
||||
|
||||
The Footnote extension implements [PHP Markdown Extra: Footnotes](https://michelf.ca/projects/php-markdown/extra/#footnotes).
|
||||
|
||||
This extension has some options:
|
||||
|
||||
| Functional option | Type | Description |
|
||||
| ----------------- | ---- | ----------- |
|
||||
| `extension.WithFootnoteIDPrefix` | `[]byte \| string` | a prefix for the id attributes.|
|
||||
| `extension.WithFootnoteIDPrefixFunction` | `func(gast.Node) []byte` | a function that determines the id attribute for given Node.|
|
||||
| `extension.WithFootnoteLinkTitle` | `[]byte \| string` | an optional title attribute for footnote links.|
|
||||
| `extension.WithFootnoteBacklinkTitle` | `[]byte \| string` | an optional title attribute for footnote backlinks. |
|
||||
| `extension.WithFootnoteLinkClass` | `[]byte \| string` | a class for footnote links. This defaults to `footnote-ref`. |
|
||||
| `extension.WithFootnoteBacklinkClass` | `[]byte \| string` | a class for footnote backlinks. This defaults to `footnote-backref`. |
|
||||
| `extension.WithFootnoteBacklinkHTML` | `[]byte \| string` | a class for footnote backlinks. This defaults to `↩︎`. |
|
||||
|
||||
Some options can have special substitutions. Occurrences of “^^” in the string will be replaced by the corresponding footnote number in the HTML output. Occurrences of “%%” will be replaced by a number for the reference (footnotes can have multiple references).
|
||||
|
||||
`extension.WithFootnoteIDPrefix` and `extension.WithFootnoteIDPrefixFunction` are useful if you have multiple Markdown documents displayed inside one HTML document to avoid footnote ids to clash each other.
|
||||
|
||||
`extension.WithFootnoteIDPrefix` sets fixed id prefix, so you may write codes like the following:
|
||||
|
||||
```go
|
||||
for _, path := range files {
|
||||
source := readAll(path)
|
||||
prefix := getPrefix(path)
|
||||
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
NewFootnote(
|
||||
WithFootnoteIDPrefix(path),
|
||||
),
|
||||
),
|
||||
)
|
||||
var b bytes.Buffer
|
||||
err := markdown.Convert(source, &b)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`extension.WithFootnoteIDPrefixFunction` determines an id prefix by calling given function, so you may write codes like the following:
|
||||
|
||||
```go
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithExtensions(
|
||||
NewFootnote(
|
||||
WithFootnoteIDPrefixFunction(func(n gast.Node) []byte {
|
||||
v, ok := n.OwnerDocument().Meta()["footnote-prefix"]
|
||||
if ok {
|
||||
return util.StringToReadOnlyBytes(v.(string))
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
for _, path := range files {
|
||||
source := readAll(path)
|
||||
var b bytes.Buffer
|
||||
|
||||
doc := markdown.Parser().Parse(text.NewReader(source))
|
||||
doc.Meta()["footnote-prefix"] = getPrefix(path)
|
||||
err := markdown.Renderer().Render(&b, source, doc)
|
||||
}
|
||||
```
|
||||
|
||||
You can use [goldmark-meta](https://github.com/yuin/goldmark-meta) to define a id prefix in the markdown document:
|
||||
|
||||
|
||||
Create extensions
|
||||
```markdown
|
||||
---
|
||||
title: document title
|
||||
slug: article1
|
||||
footnote-prefix: article1
|
||||
---
|
||||
|
||||
# My article
|
||||
|
||||
```
|
||||
|
||||
### CJK extension
|
||||
CommonMark gives compatibilities a high priority and original markdown was designed by westerners. So CommonMark lacks considerations for languages like CJK.
|
||||
|
||||
This extension provides additional options for CJK users.
|
||||
|
||||
| Functional option | Type | Description |
|
||||
| ----------------- | ---- | ----------- |
|
||||
| `extension.WithEastAsianLineBreaks` | `...extension.EastAsianLineBreaksStyle` | Soft line breaks are rendered as a newline. Some asian users will see it as an unnecessary space. With this option, soft line breaks between east asian wide characters will be ignored. This defaults to `EastAsianLineBreaksStyleSimple`. |
|
||||
| `extension.WithEscapedSpace` | `-` | Without spaces around an emphasis started with east asian punctuations, it is not interpreted as an emphasis(as defined in CommonMark spec). With this option, you can avoid this inconvenient behavior by putting 'not rendered' spaces around an emphasis like `太郎は\ **「こんにちわ」**\ といった`. |
|
||||
|
||||
#### Styles of Line Breaking
|
||||
|
||||
| Style | Description |
|
||||
| ----- | ----------- |
|
||||
| `EastAsianLineBreaksStyleSimple` | Soft line breaks are ignored if both sides of the break are east asian wide character. This behavior is the same as [`east_asian_line_breaks`](https://pandoc.org/MANUAL.html#extension-east_asian_line_breaks) in Pandoc. |
|
||||
| `EastAsianLineBreaksCSS3Draft` | This option implements CSS text level3 [Segment Break Transformation Rules](https://drafts.csswg.org/css-text-3/#line-break-transform) with [some enhancements](https://github.com/w3c/csswg-drafts/issues/5086). |
|
||||
|
||||
#### Example of `EastAsianLineBreaksStyleSimple`
|
||||
|
||||
Input Markdown:
|
||||
|
||||
```md
|
||||
私はプログラマーです。
|
||||
東京の会社に勤めています。
|
||||
GoでWebアプリケーションを開発しています。
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```html
|
||||
<p>私はプログラマーです。東京の会社に勤めています。\nGoでWebアプリケーションを開発しています。</p>
|
||||
```
|
||||
|
||||
#### Example of `EastAsianLineBreaksCSS3Draft`
|
||||
|
||||
Input Markdown:
|
||||
|
||||
```md
|
||||
私はプログラマーです。
|
||||
東京の会社に勤めています。
|
||||
GoでWebアプリケーションを開発しています。
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```html
|
||||
<p>私はプログラマーです。東京の会社に勤めています。GoでWebアプリケーションを開発しています。</p>
|
||||
```
|
||||
|
||||
Security
|
||||
--------------------
|
||||
By default, goldmark does not render raw HTML or potentially-dangerous URLs.
|
||||
If you need to gain more control over untrusted contents, it is recommended that you
|
||||
use an HTML sanitizer such as [bluemonday](https://github.com/microcosm-cc/bluemonday).
|
||||
|
||||
Benchmark
|
||||
--------------------
|
||||
You can run this benchmark in the `_benchmark` directory.
|
||||
|
||||
### against other golang libraries
|
||||
|
||||
blackfriday v2 seems to be the fastest, but as it is not CommonMark compliant, its performance cannot be directly compared to that of the CommonMark-compliant libraries.
|
||||
|
||||
goldmark, meanwhile, builds a clean, extensible AST structure, achieves full compliance with
|
||||
CommonMark, and consumes less memory, all while being reasonably fast.
|
||||
|
||||
- MBP 2019 13″(i5, 16GB), Go1.17
|
||||
|
||||
```
|
||||
BenchmarkMarkdown/Blackfriday-v2-8 302 3743747 ns/op 3290445 B/op 20050 allocs/op
|
||||
BenchmarkMarkdown/GoldMark-8 280 4200974 ns/op 2559738 B/op 13435 allocs/op
|
||||
BenchmarkMarkdown/CommonMark-8 226 5283686 ns/op 2702490 B/op 20792 allocs/op
|
||||
BenchmarkMarkdown/Lute-8 12 92652857 ns/op 10602649 B/op 40555 allocs/op
|
||||
BenchmarkMarkdown/GoMarkdown-8 13 81380167 ns/op 2245002 B/op 22889 allocs/op
|
||||
```
|
||||
|
||||
### against cmark (CommonMark reference implementation written in C)
|
||||
|
||||
- MBP 2019 13″(i5, 16GB), Go1.17
|
||||
|
||||
```
|
||||
----------- cmark -----------
|
||||
file: _data.md
|
||||
iteration: 50
|
||||
average: 0.0044073057 sec
|
||||
------- goldmark -------
|
||||
file: _data.md
|
||||
iteration: 50
|
||||
average: 0.0041611990 sec
|
||||
```
|
||||
|
||||
As you can see, goldmark's performance is on par with cmark's.
|
||||
|
||||
Extensions
|
||||
--------------------
|
||||
### List of extensions
|
||||
|
||||
- [goldmark-meta](https://github.com/yuin/goldmark-meta): A YAML metadata
|
||||
extension for the goldmark Markdown parser.
|
||||
- [goldmark-highlighting](https://github.com/yuin/goldmark-highlighting): A syntax-highlighting extension
|
||||
for the goldmark markdown parser.
|
||||
- [goldmark-emoji](https://github.com/yuin/goldmark-emoji): An emoji
|
||||
extension for the goldmark Markdown parser.
|
||||
- [goldmark-mathjax](https://github.com/litao91/goldmark-mathjax): Mathjax support for the goldmark markdown parser
|
||||
- [goldmark-pdf](https://github.com/stephenafamo/goldmark-pdf): A PDF renderer that can be passed to `goldmark.WithRenderer()`.
|
||||
- [goldmark-hashtag](https://github.com/abhinav/goldmark-hashtag): Adds support for `#hashtag`-based tagging to goldmark.
|
||||
- [goldmark-wikilink](https://github.com/abhinav/goldmark-wikilink): Adds support for `[[wiki]]`-style links to goldmark.
|
||||
- [goldmark-anchor](https://github.com/abhinav/goldmark-anchor): Adds anchors (permalinks) next to all headers in a document.
|
||||
- [goldmark-figure](https://github.com/mangoumbrella/goldmark-figure): Adds support for rendering paragraphs starting with an image to `<figure>` elements.
|
||||
- [goldmark-frontmatter](https://github.com/abhinav/goldmark-frontmatter): Adds support for YAML, TOML, and custom front matter to documents.
|
||||
- [goldmark-toc](https://github.com/abhinav/goldmark-toc): Adds support for generating tables-of-contents for goldmark documents.
|
||||
- [goldmark-mermaid](https://github.com/abhinav/goldmark-mermaid): Adds support for rendering [Mermaid](https://mermaid-js.github.io/mermaid/) diagrams in goldmark documents.
|
||||
- [goldmark-pikchr](https://github.com/jchenry/goldmark-pikchr): Adds support for rendering [Pikchr](https://pikchr.org/home/doc/trunk/homepage.md) diagrams in goldmark documents.
|
||||
- [goldmark-embed](https://github.com/13rac1/goldmark-embed): Adds support for rendering embeds from YouTube links.
|
||||
- [goldmark-latex](https://github.com/soypat/goldmark-latex): A $\LaTeX$ renderer that can be passed to `goldmark.WithRenderer()`.
|
||||
- [goldmark-fences](https://github.com/stefanfritsch/goldmark-fences): Support for pandoc-style [fenced divs](https://pandoc.org/MANUAL.html#divs-and-spans) in goldmark.
|
||||
- [goldmark-d2](https://github.com/FurqanSoftware/goldmark-d2): Adds support for [D2](https://d2lang.com/) diagrams.
|
||||
- [goldmark-katex](https://github.com/FurqanSoftware/goldmark-katex): Adds support for [KaTeX](https://katex.org/) math and equations.
|
||||
- [goldmark-img64](https://github.com/tenkoh/goldmark-img64): Adds support for embedding images into the document as DataURL (base64 encoded).
|
||||
- [goldmark-enclave](https://github.com/quailyquaily/goldmark-enclave): Adds support for embedding youtube/bilibili video, X's [oembed X](https://publish.x.com/), [tradingview chart](https://www.tradingview.com/widget/)'s chart, [quaily widget](https://quaily.com), [spotify embeds](https://developer.spotify.com/documentation/embeds), [dify embed](https://dify.ai/) and html audio into the document.
|
||||
- [goldmark-wiki-table](https://github.com/movsb/goldmark-wiki-table): Adds support for embedding Wiki Tables.
|
||||
- [goldmark-tgmd](https://github.com/Mad-Pixels/goldmark-tgmd): A Telegram markdown renderer that can be passed to `goldmark.WithRenderer()`.
|
||||
|
||||
### Loading extensions at runtime
|
||||
[goldmark-dynamic](https://github.com/yuin/goldmark-dynamic) allows you to write a goldmark extension in Lua and load it at runtime without re-compilation.
|
||||
|
||||
Please refer to [goldmark-dynamic](https://github.com/yuin/goldmark-dynamic) for details.
|
||||
|
||||
|
||||
goldmark internal(for extension developers)
|
||||
----------------------------------------------
|
||||
### Overview
|
||||
goldmark's Markdown processing is outlined in the diagram below.
|
||||
|
||||
```
|
||||
<Markdown in []byte, parser.Context>
|
||||
|
|
||||
V
|
||||
+-------- parser.Parser ---------------------------
|
||||
| 1. Parse block elements into AST
|
||||
| 1. If a parsed block is a paragraph, apply
|
||||
| ast.ParagraphTransformer
|
||||
| 2. Traverse AST and parse blocks.
|
||||
| 1. Process delimiters(emphasis) at the end of
|
||||
| block parsing
|
||||
| 3. Apply parser.ASTTransformers to AST
|
||||
|
|
||||
V
|
||||
<ast.Node>
|
||||
|
|
||||
V
|
||||
+------- renderer.Renderer ------------------------
|
||||
| 1. Traverse AST and apply renderer.NodeRenderer
|
||||
| corespond to the node type
|
||||
|
||||
|
|
||||
V
|
||||
<Output>
|
||||
```
|
||||
|
||||
### Parsing
|
||||
Markdown documents are read through `text.Reader` interface.
|
||||
|
||||
AST nodes do not have concrete text. AST nodes have segment information of the documents, represented by `text.Segment` .
|
||||
|
||||
`text.Segment` has 3 attributes: `Start`, `End`, `Padding` .
|
||||
|
||||
(TBC)
|
||||
|
||||
**TODO**
|
||||
|
||||
See `extension` directory for examples of extensions.
|
||||
|
|
@ -185,46 +554,6 @@ Summary:
|
|||
3. Write a renderer that implements `renderer.NodeRenderer`.
|
||||
4. Define your goldmark extension that implements `goldmark.Extender`.
|
||||
|
||||
Security
|
||||
--------------------
|
||||
By default, goldmark does not render raw HTMLs and potentially dangerous urls.
|
||||
If you need to gain more control over untrusted contents, it is recommended to
|
||||
use HTML sanitizer such as [bluemonday](https://github.com/microcosm-cc/bluemonday).
|
||||
|
||||
Benchmark
|
||||
--------------------
|
||||
You can run this benchmark in the `_benchmark` directory.
|
||||
|
||||
### against other golang libraries
|
||||
|
||||
blackfriday v2 seems fastest, but it is not CommonMark compiliant so performance of the
|
||||
blackfriday v2 can not simply be compared with other Commonmark compliant libraries.
|
||||
|
||||
Though goldmark builds clean extensible AST structure and get full compliance with
|
||||
Commonmark, it is resonably fast and less memory consumption.
|
||||
|
||||
```
|
||||
BenchmarkGoldMark-4 200 6388385 ns/op 2085552 B/op 13856 allocs/op
|
||||
BenchmarkGolangCommonMark-4 200 7056577 ns/op 2974119 B/op 18828 allocs/op
|
||||
BenchmarkBlackFriday-4 300 5635122 ns/op 3341668 B/op 20057 allocs/op
|
||||
```
|
||||
|
||||
### against cmark(A CommonMark reference implementation written in c)
|
||||
|
||||
```
|
||||
----------- cmark -----------
|
||||
file: _data.md
|
||||
iteration: 50
|
||||
average: 0.0050112160 sec
|
||||
go run ./goldmark_benchmark.go
|
||||
------- goldmark -------
|
||||
file: _data.md
|
||||
iteration: 50
|
||||
average: 0.0064833820 sec
|
||||
```
|
||||
|
||||
As you can see, goldmark performs pretty much equally to the cmark.
|
||||
|
||||
|
||||
Donation
|
||||
--------------------
|
||||
|
|
|
|||
1
_benchmark/cmark/.gitignore
vendored
1
_benchmark/cmark/.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
cmark-master
|
||||
cmark_benchmark
|
||||
|
|
|
|||
|
|
@ -4,18 +4,39 @@ ifeq ($(OS),Windows_NT)
|
|||
CMARK_BIN=cmark_benchmark.exe
|
||||
CMARK_RUN=bash -c "PATH=./cmark-master/build/src:$${PATH} ./$(CMARK_BIN)"
|
||||
endif
|
||||
ifneq ($(WSL_INTEROP),)
|
||||
CMARK_BIN=cmark_benchmark.exe
|
||||
CMARK_RUN=cp ./cmark-master/build-mingw/windows/bin/libcmark.dll . && ./$(CMARK_BIN); rm -f libcmark.dll
|
||||
endif
|
||||
|
||||
.PHONY: run
|
||||
|
||||
run: $(CMARK_BIN)
|
||||
$(CMARK_RUN)
|
||||
go run ./goldmark_benchmark.go
|
||||
@ $(CMARK_RUN)
|
||||
@ if [ -z "$${WSL_INTEROP}" ]; then \
|
||||
go run ./goldmark_benchmark.go; \
|
||||
else \
|
||||
GOOS=windows GOARCH=amd64 go build -o goldmark_benchmark.exe ./goldmark_benchmark.go && ./goldmark_benchmark.exe; \
|
||||
fi
|
||||
|
||||
./cmark-master/build/src/config.h:
|
||||
./cmark-master/Makefile:
|
||||
wget -nc -O cmark.zip https://github.com/commonmark/cmark/archive/master.zip
|
||||
unzip cmark.zip
|
||||
rm -f cmark.zip
|
||||
cd cmark-master && make
|
||||
@ if [ -z "$${WSL_INTEROP}" ]; then \
|
||||
cd cmark-master && make; \
|
||||
else \
|
||||
cd cmark-master && make mingw; \
|
||||
fi
|
||||
|
||||
$(CMARK_BIN): ./cmark-master/build/src/config.h
|
||||
gcc -I./cmark-master/build/src -I./cmark-master/src cmark_benchmark.c -o $(CMARK_BIN) -L./cmark-master/build/src -lcmark
|
||||
$(CMARK_BIN): ./cmark-master/Makefile
|
||||
@ if [ -z "$${WSL_INTEROP}" ]; then \
|
||||
gcc -I./cmark-master/build/src -I./cmark-master/src cmark_benchmark.c -o $(CMARK_BIN) -L./cmark-master/build/src -lcmark; \
|
||||
else \
|
||||
i686-w64-mingw32-gcc -I./cmark-master/build-mingw/windows/include cmark_benchmark.c -o $(CMARK_BIN) -L./cmark-master/build-mingw/windows/lib -lcmark.dll; \
|
||||
fi
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f $(CMARK_BIN)
|
||||
rm -f goldmark_benchmark.exe
|
||||
|
|
|
|||
|
|
@ -1,57 +1,95 @@
|
|||
package main
|
||||
package benchmark
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
gomarkdown "github.com/gomarkdown/markdown"
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
|
||||
"github.com/yuin/goldmark/util"
|
||||
"gitlab.com/golang-commonmark/markdown"
|
||||
|
||||
"gopkg.in/russross/blackfriday.v2"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
|
||||
"github.com/88250/lute"
|
||||
)
|
||||
|
||||
func BenchmarkGoldMark(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
func BenchmarkMarkdown(b *testing.B) {
|
||||
b.Run("Blackfriday-v2", func(b *testing.B) {
|
||||
r := func(src []byte) ([]byte, error) {
|
||||
out := blackfriday.Run(src)
|
||||
return out, nil
|
||||
}
|
||||
doBenchmark(b, r)
|
||||
})
|
||||
|
||||
b.Run("GoldMark", func(b *testing.B) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(html.WithXHTML(), html.WithUnsafe()),
|
||||
)
|
||||
r := func(src []byte) ([]byte, error) {
|
||||
var out bytes.Buffer
|
||||
err := markdown.Convert(src, &out)
|
||||
return out.Bytes(), err
|
||||
}
|
||||
doBenchmark(b, r)
|
||||
})
|
||||
|
||||
b.Run("CommonMark", func(b *testing.B) {
|
||||
md := markdown.New(markdown.XHTMLOutput(true))
|
||||
r := func(src []byte) ([]byte, error) {
|
||||
var out bytes.Buffer
|
||||
err := md.Render(&out, src)
|
||||
return out.Bytes(), err
|
||||
}
|
||||
doBenchmark(b, r)
|
||||
})
|
||||
|
||||
b.Run("Lute", func(b *testing.B) {
|
||||
luteEngine := lute.New()
|
||||
luteEngine.SetGFMAutoLink(false)
|
||||
luteEngine.SetGFMStrikethrough(false)
|
||||
luteEngine.SetGFMTable(false)
|
||||
luteEngine.SetGFMTaskListItem(false)
|
||||
luteEngine.SetCodeSyntaxHighlight(false)
|
||||
luteEngine.SetSoftBreak2HardBreak(false)
|
||||
luteEngine.SetAutoSpace(false)
|
||||
luteEngine.SetFixTermTypo(false)
|
||||
r := func(src []byte) ([]byte, error) {
|
||||
out := luteEngine.MarkdownStr("Benchmark", util.BytesToReadOnlyString(src))
|
||||
return util.StringToReadOnlyBytes(out), nil
|
||||
}
|
||||
doBenchmark(b, r)
|
||||
})
|
||||
|
||||
b.Run("GoMarkdown", func(b *testing.B) {
|
||||
r := func(src []byte) ([]byte, error) {
|
||||
out := gomarkdown.ToHTML(src, nil, nil)
|
||||
return out, nil
|
||||
}
|
||||
doBenchmark(b, r)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// The different frameworks have different APIs. Create an adapter that
|
||||
// should behave the same in the memory department.
|
||||
func doBenchmark(b *testing.B, render func(src []byte) ([]byte, error)) {
|
||||
b.StopTimer()
|
||||
source, err := ioutil.ReadFile("_data.md")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
b.Fatal(err)
|
||||
}
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(html.WithXHTML(), html.WithUnsafe()),
|
||||
)
|
||||
var out bytes.Buffer
|
||||
markdown.Convert([]byte(""), &out)
|
||||
|
||||
b.StartTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
out.Reset()
|
||||
if err := markdown.Convert(source, &out); err != nil {
|
||||
panic(err)
|
||||
out, err := render(source)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if len(out) < 100 {
|
||||
b.Fatal("No result")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGolangCommonMark(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
source, err := ioutil.ReadFile("_data.md")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
md := markdown.New(markdown.XHTMLOutput(true))
|
||||
for i := 0; i < b.N; i++ {
|
||||
md.RenderToString(source)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBlackFriday(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
source, err := ioutil.ReadFile("_data.md")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
blackfriday.Run(source)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
_benchmark/go/go.mod
Normal file
25
_benchmark/go/go.mod
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
module banchmark
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/88250/lute v1.7.5
|
||||
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a
|
||||
github.com/russross/blackfriday/v2 v2.1.0
|
||||
github.com/yuin/goldmark v0.0.0
|
||||
gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181 // indirect
|
||||
gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82 // indirect
|
||||
gitlab.com/golang-commonmark/mdurl v0.0.0-20191124015652-932350d1cb84 // indirect
|
||||
gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f // indirect
|
||||
golang.org/x/text v0.10.0 // indirect
|
||||
)
|
||||
|
||||
replace gopkg.in/russross/blackfriday.v2 v2.0.1 => github.com/russross/blackfriday/v2 v2.0.1
|
||||
replace github.com/yuin/goldmark v0.0.0 => ../../
|
||||
42
_benchmark/go/go.sum
Normal file
42
_benchmark/go/go.sum
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
github.com/88250/lute v1.7.5 h1:mcPFURh5sK1WH1kFRjqK5DkMWOfVN2BhyrXitN8GmpQ=
|
||||
github.com/88250/lute v1.7.5/go.mod h1:cEoBGi0zArPqAsp0MdG9SKinvH/xxZZWXU7sRx8vHSA=
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a h1:AWZzzFrqyjYlRloN6edwTLTUbKxf5flLXNuTBDm3Ews=
|
||||
github.com/gomarkdown/markdown v0.0.0-20230322041520-c84983bdbf2a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181 h1:K+bMSIx9A7mLES1rtG+qKduLIXq40DAzYHtb0XuCukA=
|
||||
gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181/go.mod h1:dzYhVIwWCtzPAa4QP98wfB9+mzt33MSmM8wsKiMi2ow=
|
||||
gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82 h1:oYrL81N608MLZhma3ruL8qTM4xcpYECGut8KSxRY59g=
|
||||
gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82/go.mod h1:Gn+LZmCrhPECMD3SOKlE+BOHwhOYD9j7WT9NUtkCrC8=
|
||||
gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a h1:O85GKETcmnCNAfv4Aym9tepU8OE0NmcZNqPlXcsBKBs=
|
||||
gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a/go.mod h1:LaSIs30YPGs1H5jwGgPhLzc8vkNc/k0rDX/fEZqiU/M=
|
||||
gitlab.com/golang-commonmark/mdurl v0.0.0-20191124015652-932350d1cb84 h1:qqjvoVXdWIcZCLPMlzgA7P9FZWdPGPvP/l3ef8GzV6o=
|
||||
gitlab.com/golang-commonmark/mdurl v0.0.0-20191124015652-932350d1cb84/go.mod h1:IJZ+fdMvbW2qW6htJx7sLJ04FEs4Ldl/MDsJtMKywfw=
|
||||
gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f h1:Wku8eEdeJqIOFHtrfkYUByc4bCaTeA6fL0UJgfEiFMI=
|
||||
gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f/go.mod h1:Tiuhl+njh/JIg0uS/sOJVYi0x2HEa5rc1OAaVsb5tAs=
|
||||
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638 h1:uPZaMiz6Sz0PZs3IZJWpU5qHKGNy///1pacZC9txiUI=
|
||||
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638/go.mod h1:EGRJaqe2eO9XGmFtQCvV3Lm9NLico3UhFwUpCG/+mVU=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
845
_test/extra.txt
Normal file
845
_test/extra.txt
Normal file
|
|
@ -0,0 +1,845 @@
|
|||
1
|
||||
//- - - - - - - - -//
|
||||
* A
|
||||
B
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>A
|
||||
B</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
2
|
||||
//- - - - - - - - -//
|
||||
**test**\
|
||||
test**test**\
|
||||
**test**test\
|
||||
test**test**
|
||||
//- - - - - - - - -//
|
||||
<p><strong>test</strong><br />
|
||||
test<strong>test</strong><br />
|
||||
<strong>test</strong>test<br />
|
||||
test<strong>test</strong></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
3
|
||||
//- - - - - - - - -//
|
||||
>* >
|
||||
> 1
|
||||
> 2
|
||||
>3
|
||||
//- - - - - - - - -//
|
||||
<blockquote>
|
||||
<ul>
|
||||
<li>
|
||||
<blockquote>
|
||||
</blockquote>
|
||||
</li>
|
||||
</ul>
|
||||
<p>1
|
||||
2
|
||||
3</p>
|
||||
</blockquote>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
4
|
||||
//- - - - - - - - -//
|
||||
`test`a`test`
|
||||
//- - - - - - - - -//
|
||||
<p><code>test</code>a<code>test</code></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
5
|
||||
//- - - - - - - - -//
|
||||
_**TL/DR** - [Go see summary.](#my-summary-area)_
|
||||
//- - - - - - - - -//
|
||||
<p><em><strong>TL/DR</strong> - <a href="#my-summary-area">Go see summary.</a></em></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
6
|
||||
//- - - - - - - - -//
|
||||
[This link won't be rendered
|
||||
correctly](https://geeksocket.in/some-long-link-here "This is the
|
||||
place where everything breaks")
|
||||
//- - - - - - - - -//
|
||||
<p><a href="https://geeksocket.in/some-long-link-here" title="This is the
|
||||
place where everything breaks">This link won't be rendered
|
||||
correctly</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
7
|
||||
//- - - - - - - - -//
|
||||
[](./target.md)
|
||||
//- - - - - - - - -//
|
||||
<p><a href="./target.md"></a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
8
|
||||
//- - - - - - - - -//
|
||||
[]()
|
||||
//- - - - - - - - -//
|
||||
<p><a href=""></a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
9
|
||||
//- - - - - - - - -//
|
||||
[daß] is the old german spelling of [dass]
|
||||
|
||||
[daß]: www.das-dass.de
|
||||
//- - - - - - - - -//
|
||||
<p><a href="www.das-dass.de">daß</a> is the old german spelling of <a href="www.das-dass.de">dass</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
10
|
||||
//- - - - - - - - -//
|
||||
1. First step.
|
||||
|
||||
~~~
|
||||
aaa
|
||||
---
|
||||
bbb
|
||||
~~~
|
||||
|
||||
2. few other steps.
|
||||
//- - - - - - - - -//
|
||||
<ol>
|
||||
<li>
|
||||
<p>First step.</p>
|
||||
<pre><code>aaa
|
||||
---
|
||||
bbb
|
||||
</code></pre>
|
||||
</li>
|
||||
<li>
|
||||
<p>few other steps.</p>
|
||||
</li>
|
||||
</ol>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
11: delimiters between ascii punctuations should be parsed
|
||||
//- - - - - - - - -//
|
||||
`{%`_name_`%}`
|
||||
//- - - - - - - - -//
|
||||
<p><code>{%</code><em>name</em><code>%}</code></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
12: the alt attribute of img should be escaped
|
||||
//- - - - - - - - -//
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
//- - - - - - - - -//
|
||||
<p><img src="quot.jpg" alt=""" />
|
||||
<img src="apos.jpg" alt="'" />
|
||||
<img src="lt.jpg" alt="<" />
|
||||
<img src="gt.jpg" alt=">" />
|
||||
<img src="amp.jpg" alt="&" /></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
13: fenced code block starting with tab inside list
|
||||
//- - - - - - - - -//
|
||||
* foo
|
||||
```Makefile
|
||||
foo
|
||||
foo
|
||||
```
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>foo
|
||||
<pre><code class="language-Makefile">foo
|
||||
foo
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
14: fenced code block inside list, mismatched tab start
|
||||
//- - - - - - - - -//
|
||||
* foo
|
||||
```Makefile
|
||||
foo
|
||||
foo
|
||||
```
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>foo
|
||||
<pre><code class="language-Makefile">foo
|
||||
foo
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
15: fenced code block inside nested list
|
||||
//- - - - - - - - -//
|
||||
* foo
|
||||
- bar
|
||||
```Makefile
|
||||
foo
|
||||
foo
|
||||
```
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>foo
|
||||
<ul>
|
||||
<li>bar
|
||||
<pre><code class="language-Makefile">foo
|
||||
foo
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
16: indented code block starting with a tab.
|
||||
//- - - - - - - - -//
|
||||
* foo
|
||||
|
||||
foo
|
||||
foo
|
||||
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>
|
||||
<p>foo</p>
|
||||
<pre><code>foo
|
||||
foo
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
17: fenced code block in list, empty line, spaces on start
|
||||
//- - - - - - - - -//
|
||||
* foo
|
||||
```Makefile
|
||||
foo
|
||||
|
||||
foo
|
||||
```
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>foo
|
||||
<pre><code class="language-Makefile">foo
|
||||
|
||||
foo
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
18: fenced code block in list, empty line, no spaces on start
|
||||
//- - - - - - - - -//
|
||||
* foo
|
||||
```Makefile
|
||||
foo
|
||||
|
||||
foo
|
||||
```
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>foo
|
||||
<pre><code class="language-Makefile">foo
|
||||
|
||||
foo
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
19: fenced code block inside nested list, empty line, spaces on start
|
||||
//- - - - - - - - -//
|
||||
* foo
|
||||
- bar
|
||||
```Makefile
|
||||
foo
|
||||
|
||||
foo
|
||||
```
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>foo
|
||||
<ul>
|
||||
<li>bar
|
||||
<pre><code class="language-Makefile">foo
|
||||
|
||||
foo
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
20: fenced code block inside nested list, empty line, no space on start
|
||||
//- - - - - - - - -//
|
||||
* foo
|
||||
- bar
|
||||
```Makefile
|
||||
foo
|
||||
|
||||
foo
|
||||
```
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>foo
|
||||
<ul>
|
||||
<li>bar
|
||||
<pre><code class="language-Makefile">foo
|
||||
|
||||
foo
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
21: Fenced code block within list can start with tab
|
||||
//- - - - - - - - -//
|
||||
- List
|
||||
|
||||
```
|
||||
A
|
||||
B
|
||||
C
|
||||
```
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>
|
||||
<p>List</p>
|
||||
<pre><code>A
|
||||
B
|
||||
C
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
22: Indented code block within list can start with tab
|
||||
//- - - - - - - - -//
|
||||
- List
|
||||
|
||||
A
|
||||
B
|
||||
C
|
||||
|
||||
a
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>
|
||||
<p>List</p>
|
||||
<pre><code>A
|
||||
B
|
||||
C
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
<p>a</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
23: Emphasis corner case(yuin/goldmark#245)
|
||||
//- - - - - - - - -//
|
||||
a* b c d *e*
|
||||
//- - - - - - - - -//
|
||||
<p>a* b c d <em>e</em></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
24: HTML block tags can contain trailing spaces
|
||||
//- - - - - - - - -//
|
||||
<aaa >
|
||||
//- - - - - - - - -//
|
||||
<aaa >
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
25: Indented code blocks can start with tab
|
||||
//- - - - - - - - -//
|
||||
x
|
||||
//- - - - - - - - -//
|
||||
<pre><code> x
|
||||
</code></pre>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
26: NUL bytes must be replaced with U+FFFD
|
||||
OPTIONS: {"enableEscape": true}
|
||||
//- - - - - - - - -//
|
||||
hello\x00world
|
||||
|
||||
<?\x00
|
||||
//- - - - - - - - -//
|
||||
<p>hello\ufffdworld</p>
|
||||
<?\uFFFD
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
27: Newlines in code spans must be preserved as a space
|
||||
OPTIONS: {"enableEscape": true}
|
||||
//- - - - - - - - -//
|
||||
`\n`
|
||||
|
||||
`x\n`
|
||||
|
||||
`\nx`
|
||||
//- - - - - - - - -//
|
||||
<p><code> </code></p>
|
||||
<p><code>x </code></p>
|
||||
<p><code> x</code></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
28: Single # is a heading level 1
|
||||
//- - - - - - - - -//
|
||||
#
|
||||
//- - - - - - - - -//
|
||||
<h1></h1>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
29: An empty list item cannot interrupt a paragraph
|
||||
//- - - - - - - - -//
|
||||
x
|
||||
*
|
||||
//- - - - - - - - -//
|
||||
<p>x
|
||||
*</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
30: A link reference definition followed by a single quote without closer
|
||||
//- - - - - - - - -//
|
||||
[x]
|
||||
|
||||
[x]: <>
|
||||
'
|
||||
//- - - - - - - - -//
|
||||
<p><a href="">x</a></p>
|
||||
<p>'</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
31: A link reference definition followed by a double quote without closer
|
||||
//- - - - - - - - -//
|
||||
[x]
|
||||
|
||||
[x]: <>
|
||||
"
|
||||
//- - - - - - - - -//
|
||||
<p><a href="">x</a></p>
|
||||
<p>"</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
32: Hex character entities must be limited to 6 characters
|
||||
//- - - - - - - - -//
|
||||
A
|
||||
//- - - - - - - - -//
|
||||
<p>&#x0000041;</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
33: \x01 should be escaped all the time
|
||||
OPTIONS: {"enableEscape": true}
|
||||
//- - - - - - - - -//
|
||||
[x](\x01)
|
||||
//- - - - - - - - -//
|
||||
<p><a href="%01">x</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
34: A form feed should not be treated as a space
|
||||
OPTIONS: {"enableEscape": true}
|
||||
//- - - - - - - - -//
|
||||
x \f
|
||||
//- - - - - - - - -//
|
||||
<p>x \f</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
35: A link reference definition can contain a new line
|
||||
//- - - - - - - - -//
|
||||
This is a [test][foo
|
||||
bar] 1...2..3...
|
||||
|
||||
[foo bar]: /
|
||||
//- - - - - - - - -//
|
||||
<p>This is a <a href="/">test</a> 1...2..3...</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
36: Emphasis and links
|
||||
//- - - - - - - - -//
|
||||
_a[b_c_](d)
|
||||
//- - - - - - - - -//
|
||||
<p>_a<a href="d">b_c_</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
37: Tabs and spaces
|
||||
OPTIONS: {"enableEscape": true}
|
||||
//- - - - - - - - -//
|
||||
\t\t x\n
|
||||
//- - - - - - - - -//
|
||||
<pre><code>\t x\n</code></pre>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
38: Decimal HTML entity literals should allow 7 digits
|
||||
//- - - - - - - - -//
|
||||
�
|
||||
//- - - - - - - - -//
|
||||
<p>\uFFFD</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
39: Decimal HTML entities should not be interpreted as octal when starting with a 0
|
||||
//- - - - - - - - -//
|
||||
d
|
||||
//- - - - - - - - -//
|
||||
<p>d</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
40: Invalid HTML tag names
|
||||
//- - - - - - - - -//
|
||||
<1>
|
||||
|
||||
<a:>
|
||||
|
||||
<a\f>
|
||||
|
||||
< p>
|
||||
//- - - - - - - - -//
|
||||
<p><1></p>
|
||||
<p><a:></p>
|
||||
<p><a\f></p>
|
||||
<p>< p></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
41: Link references can not contain spaces after link label
|
||||
//- - - - - - - - -//
|
||||
[x]
|
||||
:>
|
||||
|
||||
[o] :x
|
||||
//- - - - - - - - -//
|
||||
<p>[x]
|
||||
:></p>
|
||||
<p>[o] :x</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
42: Unclosed link reference titles can interrupt link references
|
||||
//- - - - - - - - -//
|
||||
[r]:
|
||||
<>
|
||||
'
|
||||
|
||||
[o]:
|
||||
x
|
||||
'
|
||||
//- - - - - - - - -//
|
||||
<p>'</p>
|
||||
<p>'</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
43: A link containing an image containing a link should disable the outer link
|
||||
//- - - - - - - - -//
|
||||
[  ](x) ](y)
|
||||
//- - - - - - - - -//
|
||||
<p>[ <img src="x" alt=" b " /> ](y)</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
44: An empty list item(with trailing spaces) cannot interrupt a paragraph
|
||||
//- - - - - - - - -//
|
||||
a
|
||||
*
|
||||
//- - - - - - - - -//
|
||||
<p>a
|
||||
*</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
45: Multiple empty list items
|
||||
//- - - - - - - - -//
|
||||
-
|
||||
|
||||
-
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li></li>
|
||||
<li></li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
46: Vertical tab should not be treated as spaces
|
||||
OPTIONS: {"enableEscape": true}
|
||||
//- - - - - - - - -//
|
||||
\v
|
||||
//- - - - - - - - -//
|
||||
<p>\v</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
47: Escape back slashes should not be treated as hard line breaks
|
||||
//- - - - - - - - -//
|
||||
\\\\
|
||||
a
|
||||
//- - - - - - - - -//
|
||||
<p>\
|
||||
a</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
48: Multiple paragraphs in tight list
|
||||
//- - - - - - - - -//
|
||||
- a
|
||||
>
|
||||
b
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>a
|
||||
<blockquote>
|
||||
</blockquote>
|
||||
b</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
49: A list item that is indented up to 3 spaces after an empty list item
|
||||
//- - - - - - - - -//
|
||||
1.
|
||||
|
||||
1. b
|
||||
|
||||
-
|
||||
|
||||
- b
|
||||
//- - - - - - - - -//
|
||||
<ol>
|
||||
<li></li>
|
||||
<li>
|
||||
<p>b</p>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li></li>
|
||||
<li>
|
||||
<p>b</p>
|
||||
</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
50: Spaces before a visible hard linebreak should be preserved
|
||||
//- - - - - - - - -//
|
||||
a \
|
||||
b
|
||||
//- - - - - - - - -//
|
||||
<p>a <br />
|
||||
b</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
51: Empty line in a fenced code block under list items
|
||||
//- - - - - - - - -//
|
||||
* This is a list item
|
||||
```
|
||||
This is a test
|
||||
|
||||
This line will be dropped.
|
||||
This line will be displayed.
|
||||
```
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>This is a list item
|
||||
<pre><code>This is a test
|
||||
|
||||
This line will be dropped.
|
||||
This line will be displayed.
|
||||
</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
52: windows-style newline and HTMLs
|
||||
OPTIONS: {"enableEscape": true}
|
||||
//- - - - - - - - -//
|
||||
<a \r\nhref='link'>link</a>
|
||||
|
||||
<video autoplay muted loop>\r\n<source src=\"https://example.com/example.mp4\" type=\"video/mp4\">\r\nYour browser does not support the video tag.\r\n</video>
|
||||
//- - - - - - - - -//
|
||||
<p><a \r\nhref='link'>link</a></p>
|
||||
<video autoplay muted loop>\r\n<source src=\"https://example.com/example.mp4\" type=\"video/mp4\">\r\nYour browser does not support the video tag.\r\n</video>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
53: HTML comment without trailing new lines
|
||||
OPTIONS: {"trim": true}
|
||||
//- - - - - - - - -//
|
||||
<!--
|
||||
-->
|
||||
//- - - - - - - - -//
|
||||
<!--
|
||||
-->
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
54: Escaped characters followed by a null character
|
||||
OPTIONS: {"enableEscape": true}
|
||||
//- - - - - - - - -//
|
||||
\\\x00\"
|
||||
//- - - - - - - - -//
|
||||
<p>\\\ufffd"</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
55: inline HTML comment
|
||||
//- - - - - - - - -//
|
||||
a <!-- b --> c
|
||||
|
||||
a <!-- b -->
|
||||
//- - - - - - - - -//
|
||||
<p>a <!-- b --> c</p>
|
||||
<p>a <!-- b --></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
56: An empty list followed by blockquote
|
||||
//- - - - - - - - -//
|
||||
1.
|
||||
> This is a quote.
|
||||
//- - - - - - - - -//
|
||||
<ol>
|
||||
<li></li>
|
||||
</ol>
|
||||
<blockquote>
|
||||
<p>This is a quote.</p>
|
||||
</blockquote>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
57: Tabbed fenced code block within a list
|
||||
//- - - - - - - - -//
|
||||
1.
|
||||
```
|
||||
```
|
||||
//- - - - - - - - -//
|
||||
<ol>
|
||||
<li>
|
||||
<pre><code></code></pre>
|
||||
</li>
|
||||
</ol>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
58: HTML end tag without trailing new lines
|
||||
OPTIONS: {"trim": true}
|
||||
//- - - - - - - - -//
|
||||
<pre>
|
||||
</pre>
|
||||
//- - - - - - - - -//
|
||||
<pre>
|
||||
</pre>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
59: Raw HTML tag with one new line
|
||||
//- - - - - - - - -//
|
||||
<img src=./.assets/logo.svg
|
||||
/>
|
||||
//- - - - - - - - -//
|
||||
<p><img src=./.assets/logo.svg
|
||||
/></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
60: Raw HTML tag with multiple new lines
|
||||
//- - - - - - - - -//
|
||||
<img src=./.assets/logo.svg
|
||||
|
||||
/>
|
||||
//- - - - - - - - -//
|
||||
<p><img src=./.assets/logo.svg</p>
|
||||
<p>/></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
61: Image alt with a new line
|
||||
//- - - - - - - - -//
|
||||

|
||||
//- - - - - - - - -//
|
||||
<p><img src="logo.png" alt="alt
|
||||
text" /></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
62: Image alt with an escaped character
|
||||
//- - - - - - - - -//
|
||||

|
||||
//- - - - - - - - -//
|
||||
<p><img src="https://example.com/img.png" alt="`alt" /></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
63: Emphasis in link label
|
||||
//- - - - - - - - -//
|
||||
[*[a]*](b)
|
||||
//- - - - - - - - -//
|
||||
<p><a href="b"><em>[a]</em></a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
64: Nested list under an empty list item
|
||||
//- - - - - - - - -//
|
||||
-
|
||||
- foo
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>
|
||||
<ul>
|
||||
<li>foo</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
65: Nested fenced code block with tab
|
||||
//- - - - - - - - -//
|
||||
> ```
|
||||
> 0
|
||||
> ```
|
||||
//- - - - - - - - -//
|
||||
<blockquote>
|
||||
<pre><code> 0
|
||||
</code></pre>
|
||||
</blockquote>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
66: EOF should be rendered as a newline with an unclosed block(w/ TAB)
|
||||
//- - - - - - - - -//
|
||||
> ```
|
||||
> 0
|
||||
//- - - - - - - - -//
|
||||
<blockquote>
|
||||
<pre><code> 0
|
||||
</code></pre>
|
||||
</blockquote>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
67: EOF should be rendered as a newline with an unclosed block
|
||||
//- - - - - - - - -//
|
||||
> ```
|
||||
> 0
|
||||
//- - - - - - - - -//
|
||||
<blockquote>
|
||||
<pre><code> 0
|
||||
</code></pre>
|
||||
</blockquote>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
78
_test/options.txt
Normal file
78
_test/options.txt
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
1
|
||||
//- - - - - - - - -//
|
||||
## Title 0
|
||||
|
||||
## Title1 # {#id_1 .class-1}
|
||||
|
||||
## Title2 {#id_2}
|
||||
|
||||
## Title3 ## {#id_3 .class-3}
|
||||
|
||||
## Title4 ## {data-attr3=value3}
|
||||
|
||||
## Title5 ## {#id_5 data-attr5=value5}
|
||||
|
||||
## Title6 ## {#id_6 .class6 data-attr6=value6}
|
||||
|
||||
## Title7 ## {#id_7 data-attr7="value \"7"}
|
||||
|
||||
## Title8 {#id .className data-attrName=attrValue class="class1 class2"}
|
||||
//- - - - - - - - -//
|
||||
<h2 id="title-0">Title 0</h2>
|
||||
<h2 id="id_1" class="class-1">Title1</h2>
|
||||
<h2 id="id_2">Title2</h2>
|
||||
<h2 id="id_3" class="class-3">Title3</h2>
|
||||
<h2 data-attr3="value3" id="title4">Title4</h2>
|
||||
<h2 id="id_5" data-attr5="value5">Title5</h2>
|
||||
<h2 id="id_6" class="class6" data-attr6="value6">Title6</h2>
|
||||
<h2 id="id_7" data-attr7="value "7">Title7</h2>
|
||||
<h2 id="id" class="className class1 class2" data-attrName="attrValue">Title8</h2>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
2
|
||||
//- - - - - - - - -//
|
||||
#
|
||||
# FOO
|
||||
//- - - - - - - - -//
|
||||
<h1 id="heading"></h1>
|
||||
<h1 id="foo">FOO</h1>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
3
|
||||
//- - - - - - - - -//
|
||||
## `records(self, zone, params={})`
|
||||
//- - - - - - - - -//
|
||||
<h2 id="recordsself-zone-params"><code>records(self, zone, params={})</code></h2>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
4
|
||||
//- - - - - - - - -//
|
||||
## Test {#hey .sort,class=fine,class=shell} Doesn't matter
|
||||
//- - - - - - - - -//
|
||||
<h2 id="test-hey-sortclassfineclassshell-doesnt-matter">Test {#hey .sort,class=fine,class=shell} Doesn't matter</h2>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
5
|
||||
//- - - - - - - - -//
|
||||
## Test ## {#hey .sort,class=fine,class=shell} Doesn't matter
|
||||
//- - - - - - - - -//
|
||||
<h2 id="test--hey-sortclassfineclassshell-doesnt-matter">Test ## {#hey .sort,class=fine,class=shell} Doesn't matter</h2>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
6: class must be a string
|
||||
//- - - - - - - - -//
|
||||
# Test ## {class=0#.}
|
||||
//- - - - - - - - -//
|
||||
<h1 id="test--class0">Test ## {class=0#.}</h1>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
7: short handed ids can contain hyphens ("-"), underscores ("_"), colons (":"), and periods (".")
|
||||
//- - - - - - - - -//
|
||||
# Test ## {#id-foo_bar:baz.qux .foobar}
|
||||
//- - - - - - - - -//
|
||||
<h1 id="id-foo_bar:baz.qux" class="foobar">Test</h1>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
7144
_test/spec.json
7144
_test/spec.json
File diff suppressed because it is too large
Load diff
61
_tools/build-oss-fuzz-corpus.go
Normal file
61
_tools/build-oss-fuzz-corpus.go
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type TestCase struct {
|
||||
Example int `json:"example"`
|
||||
Markdown string `json:"markdown"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
corpus_out := os.Args[1]
|
||||
if !strings.HasSuffix(corpus_out, ".zip") {
|
||||
log.Fatalln("Expected command line:", os.Args[0], "<corpus_output>.zip")
|
||||
}
|
||||
|
||||
zip_file, err := os.Create(corpus_out)
|
||||
|
||||
zip_writer := zip.NewWriter(zip_file)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalln("Failed creating file:", err)
|
||||
}
|
||||
|
||||
json_corpus := "_test/spec.json"
|
||||
bs, err := ioutil.ReadFile(json_corpus)
|
||||
if err != nil {
|
||||
log.Fatalln("Could not open file:", json_corpus)
|
||||
panic(err)
|
||||
}
|
||||
var testCases []TestCase
|
||||
if err := json.Unmarshal(bs, &testCases); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
file_in_zip := "example-" + strconv.Itoa(c.Example)
|
||||
f, err := zip_writer.Create(file_in_zip)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
_, err = f.Write([]byte(c.Markdown))
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to write file: %s into zip file", file_in_zip)
|
||||
}
|
||||
}
|
||||
|
||||
err = zip_writer.Close()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to close zip writer", err)
|
||||
}
|
||||
|
||||
zip_file.Close()
|
||||
}
|
||||
73
_tools/gen-unicode-case-folding-map.go
Normal file
73
_tools/gen-unicode-case-folding-map.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const outPath = "../util/unicode_case_folding.go"
|
||||
|
||||
type caseFolding struct {
|
||||
Class byte
|
||||
From rune
|
||||
To []rune
|
||||
}
|
||||
|
||||
func main() {
|
||||
url := "http://www.unicode.org/Public/14.0.0/ucd/CaseFolding.txt"
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to get CaseFolding.txt: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bs, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to get CaseFolding.txt: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(bs)
|
||||
scanner := bufio.NewScanner(buf)
|
||||
f, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to open %s: %v\n", outPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer f.Close()
|
||||
_, _ = f.WriteString("package util\n\n")
|
||||
_, _ = f.WriteString("var unicodeCaseFoldings = map[rune][]rune {\n")
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "#") || len(strings.TrimSpace(line)) == 0 {
|
||||
continue
|
||||
}
|
||||
line = strings.Split(line, "#")[0]
|
||||
parts := strings.Split(line, ";")
|
||||
for i, p := range parts {
|
||||
parts[i] = strings.TrimSpace(p)
|
||||
}
|
||||
cf := caseFolding{}
|
||||
v, _ := strconv.ParseInt(parts[0], 16, 32)
|
||||
cf.From = rune(int32(v))
|
||||
cf.Class = parts[1][0]
|
||||
for _, v := range strings.Split(parts[2], " ") {
|
||||
c, _ := strconv.ParseInt(v, 16, 32)
|
||||
cf.To = append(cf.To, rune(int32(c)))
|
||||
}
|
||||
if cf.Class != 'C' && cf.Class != 'F' {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(f, " %#x : %#v,\n", cf.From, cf.To)
|
||||
}
|
||||
fmt.Fprintf(f, "}\n")
|
||||
}
|
||||
149
ast/ast.go
149
ast/ast.go
|
|
@ -4,9 +4,10 @@ package ast
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
textm "github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A NodeType indicates what type a node belongs to.
|
||||
|
|
@ -38,17 +39,12 @@ func NewNodeKind(name string) NodeKind {
|
|||
return kindMax
|
||||
}
|
||||
|
||||
// An Attribute is an attribute of the Node
|
||||
// An Attribute is an attribute of the Node.
|
||||
type Attribute struct {
|
||||
Name []byte
|
||||
Value []byte
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
var attrNameIDS = []byte("#")
|
||||
var attrNameID = []byte("id")
|
||||
var attrNameClassS = []byte(".")
|
||||
var attrNameClass = []byte("class")
|
||||
|
||||
// A Node interface defines basic AST node functionalities.
|
||||
type Node interface {
|
||||
// Type returns a type of this node.
|
||||
|
|
@ -97,6 +93,9 @@ type Node interface {
|
|||
// RemoveChildren removes all children from this node.
|
||||
RemoveChildren(self Node)
|
||||
|
||||
// SortChildren sorts childrens by comparator.
|
||||
SortChildren(comparator func(n1, n2 Node) int)
|
||||
|
||||
// ReplaceChild replace a node v1 with a node insertee.
|
||||
// If v1 is not children of this node, ReplaceChild append a insetee to the
|
||||
// tail of the children.
|
||||
|
|
@ -112,6 +111,11 @@ type Node interface {
|
|||
// tail of the children.
|
||||
InsertAfter(self, v1, insertee Node)
|
||||
|
||||
// OwnerDocument returns this node's owner document.
|
||||
// If this node is not a child of the Document node, OwnerDocument
|
||||
// returns nil.
|
||||
OwnerDocument() *Document
|
||||
|
||||
// Dump dumps an AST tree structure to stdout.
|
||||
// This function completely aimed for debugging.
|
||||
// level is a indent level. Implementer should indent informations with
|
||||
|
|
@ -119,6 +123,12 @@ type Node interface {
|
|||
Dump(source []byte, level int)
|
||||
|
||||
// Text returns text values of this node.
|
||||
// This method is valid only for some inline nodes.
|
||||
// If this node is a block node, Text returns a text value as reasonable as possible.
|
||||
// Notice that there are no 'correct' text values for the block nodes.
|
||||
// Result for the block nodes may be different from your expectation.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. Pragraph.Lines, Text.Value).
|
||||
Text(source []byte) []byte
|
||||
|
||||
// HasBlankPreviousLines returns true if the row before this node is blank,
|
||||
|
|
@ -142,17 +152,20 @@ type Node interface {
|
|||
IsRaw() bool
|
||||
|
||||
// SetAttribute sets the given value to the attributes.
|
||||
SetAttribute(name, value []byte)
|
||||
SetAttribute(name []byte, value interface{})
|
||||
|
||||
// SetAttributeString sets the given value to the attributes.
|
||||
SetAttributeString(name string, value interface{})
|
||||
|
||||
// Attribute returns a (attribute value, true) if an attribute
|
||||
// associated with the given name is found, otherwise
|
||||
// (nil, false)
|
||||
Attribute(name []byte) ([]byte, bool)
|
||||
Attribute(name []byte) (interface{}, bool)
|
||||
|
||||
// AttributeString returns a (attribute value, true) if an attribute
|
||||
// associated with the given name is found, otherwise
|
||||
// (nil, false)
|
||||
AttributeString(name string) ([]byte, bool)
|
||||
AttributeString(name string) (interface{}, bool)
|
||||
|
||||
// Attributes returns a list of attributes.
|
||||
// This may be a nil if there are no attributes.
|
||||
|
|
@ -162,7 +175,7 @@ type Node interface {
|
|||
RemoveAttributes()
|
||||
}
|
||||
|
||||
// A BaseNode struct implements the Node interface.
|
||||
// A BaseNode struct implements the Node interface partialliy.
|
||||
type BaseNode struct {
|
||||
firstChild Node
|
||||
lastChild Node
|
||||
|
|
@ -229,16 +242,51 @@ func (n *BaseNode) RemoveChild(self, v Node) {
|
|||
|
||||
// RemoveChildren implements Node.RemoveChildren .
|
||||
func (n *BaseNode) RemoveChildren(self Node) {
|
||||
for c := n.firstChild; c != nil; c = c.NextSibling() {
|
||||
for c := n.firstChild; c != nil; {
|
||||
c.SetParent(nil)
|
||||
c.SetPreviousSibling(nil)
|
||||
next := c.NextSibling()
|
||||
c.SetNextSibling(nil)
|
||||
c = next
|
||||
}
|
||||
n.firstChild = nil
|
||||
n.lastChild = nil
|
||||
n.childCount = 0
|
||||
}
|
||||
|
||||
// SortChildren implements Node.SortChildren.
|
||||
func (n *BaseNode) SortChildren(comparator func(n1, n2 Node) int) {
|
||||
var sorted Node
|
||||
current := n.firstChild
|
||||
for current != nil {
|
||||
next := current.NextSibling()
|
||||
if sorted == nil || comparator(sorted, current) >= 0 {
|
||||
current.SetNextSibling(sorted)
|
||||
if sorted != nil {
|
||||
sorted.SetPreviousSibling(current)
|
||||
}
|
||||
sorted = current
|
||||
sorted.SetPreviousSibling(nil)
|
||||
} else {
|
||||
c := sorted
|
||||
for c.NextSibling() != nil && comparator(c.NextSibling(), current) < 0 {
|
||||
c = c.NextSibling()
|
||||
}
|
||||
current.SetNextSibling(c.NextSibling())
|
||||
current.SetPreviousSibling(c)
|
||||
if c.NextSibling() != nil {
|
||||
c.NextSibling().SetPreviousSibling(current)
|
||||
}
|
||||
c.SetNextSibling(current)
|
||||
}
|
||||
current = next
|
||||
}
|
||||
n.firstChild = sorted
|
||||
for c := n.firstChild; c != nil; c = c.NextSibling() {
|
||||
n.lastChild = c
|
||||
}
|
||||
}
|
||||
|
||||
// FirstChild implements Node.FirstChild .
|
||||
func (n *BaseNode) FirstChild() Node {
|
||||
return n.firstChild
|
||||
|
|
@ -316,17 +364,40 @@ func (n *BaseNode) InsertBefore(self, v1, insertee Node) {
|
|||
}
|
||||
}
|
||||
|
||||
// Text implements Node.Text .
|
||||
// OwnerDocument implements Node.OwnerDocument.
|
||||
func (n *BaseNode) OwnerDocument() *Document {
|
||||
d := n.Parent()
|
||||
for {
|
||||
p := d.Parent()
|
||||
if p == nil {
|
||||
if v, ok := d.(*Document); ok {
|
||||
return v
|
||||
}
|
||||
break
|
||||
}
|
||||
d = p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Text implements Node.Text .
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. Pragraph.Lines, Text.Value).
|
||||
func (n *BaseNode) Text(source []byte) []byte {
|
||||
var buf bytes.Buffer
|
||||
for c := n.firstChild; c != nil; c = c.NextSibling() {
|
||||
buf.Write(c.Text(source))
|
||||
if sb, ok := c.(interface {
|
||||
SoftLineBreak() bool
|
||||
}); ok && sb.SoftLineBreak() {
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// SetAttribute implements Node.SetAttribute.
|
||||
func (n *BaseNode) SetAttribute(name, value []byte) {
|
||||
func (n *BaseNode) SetAttribute(name []byte, value interface{}) {
|
||||
if n.attributes == nil {
|
||||
n.attributes = make([]Attribute, 0, 10)
|
||||
} else {
|
||||
|
|
@ -338,21 +409,16 @@ func (n *BaseNode) SetAttribute(name, value []byte) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if len(name) == 1 {
|
||||
if name[0] == '#' {
|
||||
n.attributes = append(n.attributes, Attribute{attrNameID, value})
|
||||
return
|
||||
} else if name[0] == '.' {
|
||||
n.attributes = append(n.attributes, Attribute{attrNameClass, value})
|
||||
return
|
||||
}
|
||||
}
|
||||
n.attributes = append(n.attributes, Attribute{name, value})
|
||||
return
|
||||
}
|
||||
|
||||
// SetAttributeString implements Node.SetAttributeString.
|
||||
func (n *BaseNode) SetAttributeString(name string, value interface{}) {
|
||||
n.SetAttribute(util.StringToReadOnlyBytes(name), value)
|
||||
}
|
||||
|
||||
// Attribute implements Node.Attribute.
|
||||
func (n *BaseNode) Attribute(name []byte) ([]byte, bool) {
|
||||
func (n *BaseNode) Attribute(name []byte) (interface{}, bool) {
|
||||
if n.attributes == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
|
@ -365,16 +431,16 @@ func (n *BaseNode) Attribute(name []byte) ([]byte, bool) {
|
|||
}
|
||||
|
||||
// AttributeString implements Node.AttributeString.
|
||||
func (n *BaseNode) AttributeString(s string) ([]byte, bool) {
|
||||
func (n *BaseNode) AttributeString(s string) (interface{}, bool) {
|
||||
return n.Attribute(util.StringToReadOnlyBytes(s))
|
||||
}
|
||||
|
||||
// Attributes implements Node.Attributes
|
||||
// Attributes implements Node.Attributes.
|
||||
func (n *BaseNode) Attributes() []Attribute {
|
||||
return n.attributes
|
||||
}
|
||||
|
||||
// RemoveAttributes implements Node.RemoveAttributes
|
||||
// RemoveAttributes implements Node.RemoveAttributes.
|
||||
func (n *BaseNode) RemoveAttributes() {
|
||||
n.attributes = nil
|
||||
}
|
||||
|
|
@ -396,10 +462,8 @@ func DumpHelper(v Node, source []byte, level int, kv map[string]string, cb func(
|
|||
fmt.Printf("\"\n")
|
||||
fmt.Printf("%sHasBlankPreviousLines: %v\n", indent2, v.HasBlankPreviousLines())
|
||||
}
|
||||
if kv != nil {
|
||||
for name, value := range kv {
|
||||
fmt.Printf("%s%s: %s\n", indent2, name, value)
|
||||
}
|
||||
for name, value := range kv {
|
||||
fmt.Printf("%s%s: %s\n", indent2, name, value)
|
||||
}
|
||||
if cb != nil {
|
||||
cb(level + 1)
|
||||
|
|
@ -415,7 +479,7 @@ type WalkStatus int
|
|||
|
||||
const (
|
||||
// WalkStop indicates no more walking needed.
|
||||
WalkStop = iota + 1
|
||||
WalkStop WalkStatus = iota + 1
|
||||
|
||||
// WalkSkipChildren indicates that Walk wont walk on children of current
|
||||
// node.
|
||||
|
|
@ -431,22 +495,27 @@ const (
|
|||
// If Walker returns error, Walk function immediately stop walking.
|
||||
type Walker func(n Node, entering bool) (WalkStatus, error)
|
||||
|
||||
// Walk walks a AST tree by the depth first search algorighm.
|
||||
// Walk walks a AST tree by the depth first search algorithm.
|
||||
func Walk(n Node, walker Walker) error {
|
||||
_, err := walkHelper(n, walker)
|
||||
return err
|
||||
}
|
||||
|
||||
func walkHelper(n Node, walker Walker) (WalkStatus, error) {
|
||||
status, err := walker(n, true)
|
||||
if err != nil || status == WalkStop {
|
||||
return err
|
||||
return status, err
|
||||
}
|
||||
if status != WalkSkipChildren {
|
||||
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
||||
if err := Walk(c, walker); err != nil {
|
||||
return err
|
||||
if st, err := walkHelper(c, walker); err != nil || st == WalkStop {
|
||||
return WalkStop, err
|
||||
}
|
||||
}
|
||||
}
|
||||
status, err = walker(n, false)
|
||||
if err != nil || status == WalkStop {
|
||||
return err
|
||||
return WalkStop, err
|
||||
}
|
||||
return nil
|
||||
return WalkContinue, nil
|
||||
}
|
||||
|
|
|
|||
60
ast/ast_test.go
Normal file
60
ast/ast_test.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package ast
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWalk(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
node Node
|
||||
want []NodeKind
|
||||
action map[NodeKind]WalkStatus
|
||||
}{
|
||||
{
|
||||
"visits all in depth first order",
|
||||
node(NewDocument(), node(NewHeading(1), NewText()), NewLink()),
|
||||
[]NodeKind{KindDocument, KindHeading, KindText, KindLink},
|
||||
map[NodeKind]WalkStatus{},
|
||||
},
|
||||
{
|
||||
"stops after heading",
|
||||
node(NewDocument(), node(NewHeading(1), NewText()), NewLink()),
|
||||
[]NodeKind{KindDocument, KindHeading},
|
||||
map[NodeKind]WalkStatus{KindHeading: WalkStop},
|
||||
},
|
||||
{
|
||||
"skip children",
|
||||
node(NewDocument(), node(NewHeading(1), NewText()), NewLink()),
|
||||
[]NodeKind{KindDocument, KindHeading, KindLink},
|
||||
map[NodeKind]WalkStatus{KindHeading: WalkSkipChildren},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var kinds []NodeKind
|
||||
collectKinds := func(n Node, entering bool) (WalkStatus, error) {
|
||||
if entering {
|
||||
kinds = append(kinds, n.Kind())
|
||||
}
|
||||
if status, ok := tt.action[n.Kind()]; ok {
|
||||
return status, nil
|
||||
}
|
||||
return WalkContinue, nil
|
||||
}
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := Walk(tt.node, collectKinds); err != nil {
|
||||
t.Errorf("Walk() error = %v", err)
|
||||
} else if !reflect.DeepEqual(kinds, tt.want) {
|
||||
t.Errorf("Walk() expected = %v, got = %v", tt.want, kinds)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func node(n Node, children ...Node) Node {
|
||||
for _, c := range children {
|
||||
n.AppendChild(n, c)
|
||||
}
|
||||
return n
|
||||
}
|
||||
153
ast/block.go
153
ast/block.go
|
|
@ -2,23 +2,24 @@ package ast
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
textm "github.com/yuin/goldmark/text"
|
||||
"strings"
|
||||
|
||||
textm "github.com/yuin/goldmark/text"
|
||||
)
|
||||
|
||||
// A BaseBlock struct implements the Node interface.
|
||||
// A BaseBlock struct implements the Node interface partialliy.
|
||||
type BaseBlock struct {
|
||||
BaseNode
|
||||
blankPreviousLines bool
|
||||
lines *textm.Segments
|
||||
}
|
||||
|
||||
// Type implements Node.Type
|
||||
// Type implements Node.Type.
|
||||
func (b *BaseBlock) Type() NodeType {
|
||||
return TypeBlock
|
||||
}
|
||||
|
||||
// IsRaw implements Node.IsRaw
|
||||
// IsRaw implements Node.IsRaw.
|
||||
func (b *BaseBlock) IsRaw() bool {
|
||||
return false
|
||||
}
|
||||
|
|
@ -33,7 +34,7 @@ func (b *BaseBlock) SetBlankPreviousLines(v bool) {
|
|||
b.blankPreviousLines = v
|
||||
}
|
||||
|
||||
// Lines implements Node.Lines
|
||||
// Lines implements Node.Lines.
|
||||
func (b *BaseBlock) Lines() *textm.Segments {
|
||||
if b.lines == nil {
|
||||
b.lines = textm.NewSegments()
|
||||
|
|
@ -41,7 +42,7 @@ func (b *BaseBlock) Lines() *textm.Segments {
|
|||
return b.lines
|
||||
}
|
||||
|
||||
// SetLines implements Node.SetLines
|
||||
// SetLines implements Node.SetLines.
|
||||
func (b *BaseBlock) SetLines(v *textm.Segments) {
|
||||
b.lines = v
|
||||
}
|
||||
|
|
@ -49,12 +50,14 @@ func (b *BaseBlock) SetLines(v *textm.Segments) {
|
|||
// A Document struct is a root node of Markdown text.
|
||||
type Document struct {
|
||||
BaseBlock
|
||||
|
||||
meta map[string]interface{}
|
||||
}
|
||||
|
||||
// KindDocument is a NodeKind of the Document node.
|
||||
var KindDocument = NewNodeKind("Document")
|
||||
|
||||
// Dump impelements Node.Dump .
|
||||
// Dump implements Node.Dump .
|
||||
func (n *Document) Dump(source []byte, level int) {
|
||||
DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
|
@ -69,10 +72,42 @@ func (n *Document) Kind() NodeKind {
|
|||
return KindDocument
|
||||
}
|
||||
|
||||
// OwnerDocument implements Node.OwnerDocument.
|
||||
func (n *Document) OwnerDocument() *Document {
|
||||
return n
|
||||
}
|
||||
|
||||
// Meta returns metadata of this document.
|
||||
func (n *Document) Meta() map[string]interface{} {
|
||||
if n.meta == nil {
|
||||
n.meta = map[string]interface{}{}
|
||||
}
|
||||
return n.meta
|
||||
}
|
||||
|
||||
// SetMeta sets given metadata to this document.
|
||||
func (n *Document) SetMeta(meta map[string]interface{}) {
|
||||
if n.meta == nil {
|
||||
n.meta = map[string]interface{}{}
|
||||
}
|
||||
for k, v := range meta {
|
||||
n.meta[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// AddMeta adds given metadata to this document.
|
||||
func (n *Document) AddMeta(key string, value interface{}) {
|
||||
if n.meta == nil {
|
||||
n.meta = map[string]interface{}{}
|
||||
}
|
||||
n.meta[key] = value
|
||||
}
|
||||
|
||||
// NewDocument returns a new Document node.
|
||||
func NewDocument() *Document {
|
||||
return &Document{
|
||||
BaseBlock: BaseBlock{},
|
||||
meta: nil,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -82,7 +117,7 @@ type TextBlock struct {
|
|||
BaseBlock
|
||||
}
|
||||
|
||||
// Dump impelements Node.Dump .
|
||||
// Dump implements Node.Dump .
|
||||
func (n *TextBlock) Dump(source []byte, level int) {
|
||||
DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
|
@ -95,6 +130,13 @@ func (n *TextBlock) Kind() NodeKind {
|
|||
return KindTextBlock
|
||||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. TextBlock.Lines).
|
||||
func (n *TextBlock) Text(source []byte) []byte {
|
||||
return n.Lines().Value(source)
|
||||
}
|
||||
|
||||
// NewTextBlock returns a new TextBlock node.
|
||||
func NewTextBlock() *TextBlock {
|
||||
return &TextBlock{
|
||||
|
|
@ -107,7 +149,7 @@ type Paragraph struct {
|
|||
BaseBlock
|
||||
}
|
||||
|
||||
// Dump impelements Node.Dump .
|
||||
// Dump implements Node.Dump .
|
||||
func (n *Paragraph) Dump(source []byte, level int) {
|
||||
DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
|
@ -120,6 +162,13 @@ func (n *Paragraph) Kind() NodeKind {
|
|||
return KindParagraph
|
||||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. Paragraph.Lines).
|
||||
func (n *Paragraph) Text(source []byte) []byte {
|
||||
return n.Lines().Value(source)
|
||||
}
|
||||
|
||||
// NewParagraph returns a new Paragraph node.
|
||||
func NewParagraph() *Paragraph {
|
||||
return &Paragraph{
|
||||
|
|
@ -142,7 +191,7 @@ type Heading struct {
|
|||
Level int
|
||||
}
|
||||
|
||||
// Dump impelements Node.Dump .
|
||||
// Dump implements Node.Dump .
|
||||
func (n *Heading) Dump(source []byte, level int) {
|
||||
m := map[string]string{
|
||||
"Level": fmt.Sprintf("%d", n.Level),
|
||||
|
|
@ -166,27 +215,27 @@ func NewHeading(level int) *Heading {
|
|||
}
|
||||
}
|
||||
|
||||
// A ThemanticBreak struct represents a themantic break of Markdown text.
|
||||
type ThemanticBreak struct {
|
||||
// A ThematicBreak struct represents a thematic break of Markdown text.
|
||||
type ThematicBreak struct {
|
||||
BaseBlock
|
||||
}
|
||||
|
||||
// Dump impelements Node.Dump .
|
||||
func (n *ThemanticBreak) Dump(source []byte, level int) {
|
||||
// Dump implements Node.Dump .
|
||||
func (n *ThematicBreak) Dump(source []byte, level int) {
|
||||
DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
||||
// KindThemanticBreak is a NodeKind of the ThemanticBreak node.
|
||||
var KindThemanticBreak = NewNodeKind("ThemanticBreak")
|
||||
// KindThematicBreak is a NodeKind of the ThematicBreak node.
|
||||
var KindThematicBreak = NewNodeKind("ThematicBreak")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *ThemanticBreak) Kind() NodeKind {
|
||||
return KindThemanticBreak
|
||||
func (n *ThematicBreak) Kind() NodeKind {
|
||||
return KindThematicBreak
|
||||
}
|
||||
|
||||
// NewThemanticBreak returns a new ThemanticBreak node.
|
||||
func NewThemanticBreak() *ThemanticBreak {
|
||||
return &ThemanticBreak{
|
||||
// NewThematicBreak returns a new ThematicBreak node.
|
||||
func NewThematicBreak() *ThematicBreak {
|
||||
return &ThematicBreak{
|
||||
BaseBlock: BaseBlock{},
|
||||
}
|
||||
}
|
||||
|
|
@ -201,7 +250,7 @@ func (n *CodeBlock) IsRaw() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// Dump impelements Node.Dump .
|
||||
// Dump implements Node.Dump .
|
||||
func (n *CodeBlock) Dump(source []byte, level int) {
|
||||
DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
|
@ -214,6 +263,13 @@ func (n *CodeBlock) Kind() NodeKind {
|
|||
return KindCodeBlock
|
||||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. CodeBlock.Lines).
|
||||
func (n *CodeBlock) Text(source []byte) []byte {
|
||||
return n.Lines().Value(source)
|
||||
}
|
||||
|
||||
// NewCodeBlock returns a new CodeBlock node.
|
||||
func NewCodeBlock() *CodeBlock {
|
||||
return &CodeBlock{
|
||||
|
|
@ -252,7 +308,7 @@ func (n *FencedCodeBlock) IsRaw() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// Dump impelements Node.Dump .
|
||||
// Dump implements Node.Dump .
|
||||
func (n *FencedCodeBlock) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
if n.Info != nil {
|
||||
|
|
@ -269,6 +325,13 @@ func (n *FencedCodeBlock) Kind() NodeKind {
|
|||
return KindFencedCodeBlock
|
||||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. FencedCodeBlock.Lines).
|
||||
func (n *FencedCodeBlock) Text(source []byte) []byte {
|
||||
return n.Lines().Value(source)
|
||||
}
|
||||
|
||||
// NewFencedCodeBlock return a new FencedCodeBlock node.
|
||||
func NewFencedCodeBlock(info *Text) *FencedCodeBlock {
|
||||
return &FencedCodeBlock{
|
||||
|
|
@ -282,7 +345,7 @@ type Blockquote struct {
|
|||
BaseBlock
|
||||
}
|
||||
|
||||
// Dump impelements Node.Dump .
|
||||
// Dump implements Node.Dump .
|
||||
func (n *Blockquote) Dump(source []byte, level int) {
|
||||
DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
|
@ -302,15 +365,15 @@ func NewBlockquote() *Blockquote {
|
|||
}
|
||||
}
|
||||
|
||||
// A List structr represents a list of Markdown text.
|
||||
// A List struct represents a list of Markdown text.
|
||||
type List struct {
|
||||
BaseBlock
|
||||
|
||||
// Marker is a markar character like '-', '+', ')' and '.'.
|
||||
// Marker is a marker character like '-', '+', ')' and '.'.
|
||||
Marker byte
|
||||
|
||||
// IsTight is a true if this list is a 'tight' list.
|
||||
// See https://spec.commonmark.org/0.29/#loose for details.
|
||||
// See https://spec.commonmark.org/0.30/#loose for details.
|
||||
IsTight bool
|
||||
|
||||
// Start is an initial number of this ordered list.
|
||||
|
|
@ -363,13 +426,16 @@ func NewList(marker byte) *List {
|
|||
type ListItem struct {
|
||||
BaseBlock
|
||||
|
||||
// Offset is an offset potision of this item.
|
||||
// Offset is an offset position of this item.
|
||||
Offset int
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *ListItem) Dump(source []byte, level int) {
|
||||
DumpHelper(n, source, level, nil, nil)
|
||||
m := map[string]string{
|
||||
"Offset": fmt.Sprintf("%d", n.Offset),
|
||||
}
|
||||
DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
// KindListItem is a NodeKind of the ListItem node.
|
||||
|
|
@ -389,23 +455,23 @@ func NewListItem(offset int) *ListItem {
|
|||
}
|
||||
|
||||
// HTMLBlockType represents kinds of an html blocks.
|
||||
// See https://spec.commonmark.org/0.29/#html-blocks
|
||||
// See https://spec.commonmark.org/0.30/#html-blocks
|
||||
type HTMLBlockType int
|
||||
|
||||
const (
|
||||
// HTMLBlockType1 represents type 1 html blocks
|
||||
HTMLBlockType1 = iota + 1
|
||||
// HTMLBlockType2 represents type 2 html blocks
|
||||
// HTMLBlockType1 represents type 1 html blocks.
|
||||
HTMLBlockType1 HTMLBlockType = iota + 1
|
||||
// HTMLBlockType2 represents type 2 html blocks.
|
||||
HTMLBlockType2
|
||||
// HTMLBlockType3 represents type 3 html blocks
|
||||
// HTMLBlockType3 represents type 3 html blocks.
|
||||
HTMLBlockType3
|
||||
// HTMLBlockType4 represents type 4 html blocks
|
||||
// HTMLBlockType4 represents type 4 html blocks.
|
||||
HTMLBlockType4
|
||||
// HTMLBlockType5 represents type 5 html blocks
|
||||
// HTMLBlockType5 represents type 5 html blocks.
|
||||
HTMLBlockType5
|
||||
// HTMLBlockType6 represents type 6 html blocks
|
||||
// HTMLBlockType6 represents type 6 html blocks.
|
||||
HTMLBlockType6
|
||||
// HTMLBlockType7 represents type 7 html blocks
|
||||
// HTMLBlockType7 represents type 7 html blocks.
|
||||
HTMLBlockType7
|
||||
)
|
||||
|
||||
|
|
@ -460,6 +526,17 @@ func (n *HTMLBlock) Kind() NodeKind {
|
|||
return KindHTMLBlock
|
||||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. HTMLBlock.Lines).
|
||||
func (n *HTMLBlock) Text(source []byte) []byte {
|
||||
ret := n.Lines().Value(source)
|
||||
if n.HasClosure() {
|
||||
ret = append(ret, n.ClosureLine.Value(source)...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// NewHTMLBlock returns a new HTMLBlock node.
|
||||
func NewHTMLBlock(typ HTMLBlockType) *HTMLBlock {
|
||||
return &HTMLBlock{
|
||||
|
|
|
|||
173
ast/inline.go
173
ast/inline.go
|
|
@ -8,17 +8,17 @@ import (
|
|||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
// A BaseInline struct implements the Node interface.
|
||||
// A BaseInline struct implements the Node interface partialliy.
|
||||
type BaseInline struct {
|
||||
BaseNode
|
||||
}
|
||||
|
||||
// Type implements Node.Type
|
||||
// Type implements Node.Type.
|
||||
func (b *BaseInline) Type() NodeType {
|
||||
return TypeInline
|
||||
}
|
||||
|
||||
// IsRaw implements Node.IsRaw
|
||||
// IsRaw implements Node.IsRaw.
|
||||
func (b *BaseInline) IsRaw() bool {
|
||||
return false
|
||||
}
|
||||
|
|
@ -33,12 +33,12 @@ func (b *BaseInline) SetBlankPreviousLines(v bool) {
|
|||
panic("can not call with inline nodes.")
|
||||
}
|
||||
|
||||
// Lines implements Node.Lines
|
||||
// Lines implements Node.Lines.
|
||||
func (b *BaseInline) Lines() *textm.Segments {
|
||||
panic("can not call with inline nodes.")
|
||||
}
|
||||
|
||||
// SetLines implements Node.SetLines
|
||||
// SetLines implements Node.SetLines.
|
||||
func (b *BaseInline) SetLines(v *textm.Segments) {
|
||||
panic("can not call with inline nodes.")
|
||||
}
|
||||
|
|
@ -56,8 +56,26 @@ const (
|
|||
textSoftLineBreak = 1 << iota
|
||||
textHardLineBreak
|
||||
textRaw
|
||||
textCode
|
||||
)
|
||||
|
||||
func textFlagsString(flags uint8) string {
|
||||
buf := []string{}
|
||||
if flags&textSoftLineBreak != 0 {
|
||||
buf = append(buf, "SoftLineBreak")
|
||||
}
|
||||
if flags&textHardLineBreak != 0 {
|
||||
buf = append(buf, "HardLineBreak")
|
||||
}
|
||||
if flags&textRaw != 0 {
|
||||
buf = append(buf, "Raw")
|
||||
}
|
||||
if flags&textCode != 0 {
|
||||
buf = append(buf, "Code")
|
||||
}
|
||||
return strings.Join(buf, ", ")
|
||||
}
|
||||
|
||||
// Inline implements Inline.Inline.
|
||||
func (n *Text) Inline() {
|
||||
}
|
||||
|
|
@ -73,7 +91,7 @@ func (n *Text) SetSoftLineBreak(v bool) {
|
|||
if v {
|
||||
n.flags |= textSoftLineBreak
|
||||
} else {
|
||||
n.flags = n.flags &^ textHardLineBreak
|
||||
n.flags = n.flags &^ textSoftLineBreak
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +111,7 @@ func (n *Text) SetRaw(v bool) {
|
|||
}
|
||||
|
||||
// HardLineBreak returns true if this node ends with a hard line break.
|
||||
// See https://spec.commonmark.org/0.29/#hard-line-breaks for details.
|
||||
// See https://spec.commonmark.org/0.30/#hard-line-breaks for details.
|
||||
func (n *Text) HardLineBreak() bool {
|
||||
return n.flags&textHardLineBreak != 0
|
||||
}
|
||||
|
|
@ -114,7 +132,8 @@ func (n *Text) Merge(node Node, source []byte) bool {
|
|||
if !ok {
|
||||
return false
|
||||
}
|
||||
if n.Segment.Stop != t.Segment.Start || t.Segment.Padding != 0 || source[n.Segment.Stop-1] == '\n' || t.IsRaw() != n.IsRaw() {
|
||||
if n.Segment.Stop != t.Segment.Start || t.Segment.Padding != 0 ||
|
||||
source[n.Segment.Stop-1] == '\n' || t.IsRaw() != n.IsRaw() {
|
||||
return false
|
||||
}
|
||||
n.Segment.Stop = t.Segment.Stop
|
||||
|
|
@ -124,13 +143,25 @@ func (n *Text) Merge(node Node, source []byte) bool {
|
|||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. Text.Value).
|
||||
func (n *Text) Text(source []byte) []byte {
|
||||
return n.Segment.Value(source)
|
||||
}
|
||||
|
||||
// Value returns a value of this node.
|
||||
// SoftLineBreaks are not included in the returned value.
|
||||
func (n *Text) Value(source []byte) []byte {
|
||||
return n.Segment.Value(source)
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *Text) Dump(source []byte, level int) {
|
||||
fmt.Printf("%sText: \"%s\"\n", strings.Repeat(" ", level), strings.TrimRight(string(n.Text(source)), "\n"))
|
||||
fs := textFlagsString(n.flags)
|
||||
if len(fs) != 0 {
|
||||
fs = "(" + fs + ")"
|
||||
}
|
||||
fmt.Printf("%sText%s: \"%s\"\n", strings.Repeat(" ", level), fs, strings.TrimRight(string(n.Value(source)), "\n"))
|
||||
}
|
||||
|
||||
// KindText is a NodeKind of the Text node.
|
||||
|
|
@ -148,7 +179,7 @@ func NewText() *Text {
|
|||
}
|
||||
}
|
||||
|
||||
// NewTextSegment returns a new Text node with the given source potision.
|
||||
// NewTextSegment returns a new Text node with the given source position.
|
||||
func NewTextSegment(v textm.Segment) *Text {
|
||||
return &Text{
|
||||
BaseInline: BaseInline{},
|
||||
|
|
@ -192,6 +223,79 @@ func MergeOrReplaceTextSegment(parent Node, n Node, s textm.Segment) {
|
|||
}
|
||||
}
|
||||
|
||||
// A String struct is a textual content that has a concrete value.
|
||||
type String struct {
|
||||
BaseInline
|
||||
|
||||
Value []byte
|
||||
flags uint8
|
||||
}
|
||||
|
||||
// Inline implements Inline.Inline.
|
||||
func (n *String) Inline() {
|
||||
}
|
||||
|
||||
// IsRaw returns true if this text should be rendered without unescaping
|
||||
// back slash escapes and resolving references.
|
||||
func (n *String) IsRaw() bool {
|
||||
return n.flags&textRaw != 0
|
||||
}
|
||||
|
||||
// SetRaw sets whether this text should be rendered as raw contents.
|
||||
func (n *String) SetRaw(v bool) {
|
||||
if v {
|
||||
n.flags |= textRaw
|
||||
} else {
|
||||
n.flags = n.flags &^ textRaw
|
||||
}
|
||||
}
|
||||
|
||||
// IsCode returns true if this text should be rendered without any
|
||||
// modifications.
|
||||
func (n *String) IsCode() bool {
|
||||
return n.flags&textCode != 0
|
||||
}
|
||||
|
||||
// SetCode sets whether this text should be rendered without any modifications.
|
||||
func (n *String) SetCode(v bool) {
|
||||
if v {
|
||||
n.flags |= textCode
|
||||
} else {
|
||||
n.flags = n.flags &^ textCode
|
||||
}
|
||||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. String.Value).
|
||||
func (n *String) Text(source []byte) []byte {
|
||||
return n.Value
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *String) Dump(source []byte, level int) {
|
||||
fs := textFlagsString(n.flags)
|
||||
if len(fs) != 0 {
|
||||
fs = "(" + fs + ")"
|
||||
}
|
||||
fmt.Printf("%sString%s: \"%s\"\n", strings.Repeat(" ", level), fs, strings.TrimRight(string(n.Value), "\n"))
|
||||
}
|
||||
|
||||
// KindString is a NodeKind of the String node.
|
||||
var KindString = NewNodeKind("String")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *String) Kind() NodeKind {
|
||||
return KindString
|
||||
}
|
||||
|
||||
// NewString returns a new String node.
|
||||
func NewString(v []byte) *String {
|
||||
return &String{
|
||||
Value: v,
|
||||
}
|
||||
}
|
||||
|
||||
// A CodeSpan struct represents a code span of Markdown text.
|
||||
type CodeSpan struct {
|
||||
BaseInline
|
||||
|
|
@ -212,7 +316,7 @@ func (n *CodeSpan) IsBlank(source []byte) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump
|
||||
// Dump implements Node.Dump.
|
||||
func (n *CodeSpan) Dump(source []byte, level int) {
|
||||
DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
|
@ -354,7 +458,7 @@ type AutoLinkType int
|
|||
|
||||
const (
|
||||
// AutoLinkEmail indicates that an autolink is an email address.
|
||||
AutoLinkEmail = iota + 1
|
||||
AutoLinkEmail AutoLinkType = iota + 1
|
||||
// AutoLinkURL indicates that an autolink is a generic URL.
|
||||
AutoLinkURL
|
||||
)
|
||||
|
|
@ -362,18 +466,21 @@ const (
|
|||
// An AutoLink struct represents an autolink of the Markdown text.
|
||||
type AutoLink struct {
|
||||
BaseInline
|
||||
// Value is a link text of this node.
|
||||
Value *Text
|
||||
// Type is a type of this autolink.
|
||||
AutoLinkType AutoLinkType
|
||||
|
||||
// Protocol specified a protocol of the link.
|
||||
Protocol []byte
|
||||
|
||||
value *Text
|
||||
}
|
||||
|
||||
// Inline implements Inline.Inline.
|
||||
func (n *AutoLink) Inline() {}
|
||||
|
||||
// Dump implenets Node.Dump
|
||||
// Dump implements Node.Dump.
|
||||
func (n *AutoLink) Dump(source []byte, level int) {
|
||||
segment := n.Value.Segment
|
||||
segment := n.value.Segment
|
||||
m := map[string]string{
|
||||
"Value": string(segment.Value(source)),
|
||||
}
|
||||
|
|
@ -388,11 +495,36 @@ func (n *AutoLink) Kind() NodeKind {
|
|||
return KindAutoLink
|
||||
}
|
||||
|
||||
// URL returns an url of this node.
|
||||
func (n *AutoLink) URL(source []byte) []byte {
|
||||
if n.Protocol != nil {
|
||||
s := n.value.Segment
|
||||
ret := make([]byte, 0, len(n.Protocol)+s.Len()+3)
|
||||
ret = append(ret, n.Protocol...)
|
||||
ret = append(ret, ':', '/', '/')
|
||||
ret = append(ret, n.value.Value(source)...)
|
||||
return ret
|
||||
}
|
||||
return n.value.Value(source)
|
||||
}
|
||||
|
||||
// Label returns a label of this node.
|
||||
func (n *AutoLink) Label(source []byte) []byte {
|
||||
return n.value.Value(source)
|
||||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. AutoLink.Label).
|
||||
func (n *AutoLink) Text(source []byte) []byte {
|
||||
return n.value.Value(source)
|
||||
}
|
||||
|
||||
// NewAutoLink returns a new AutoLink node.
|
||||
func NewAutoLink(typ AutoLinkType, value *Text) *AutoLink {
|
||||
return &AutoLink{
|
||||
BaseInline: BaseInline{},
|
||||
Value: value,
|
||||
value: value,
|
||||
AutoLinkType: typ,
|
||||
}
|
||||
}
|
||||
|
|
@ -426,6 +558,13 @@ func (n *RawHTML) Kind() NodeKind {
|
|||
return KindRawHTML
|
||||
}
|
||||
|
||||
// Text implements Node.Text.
|
||||
//
|
||||
// Deprecated: Use other properties of the node to get the text value(i.e. RawHTML.Segments).
|
||||
func (n *RawHTML) Text(source []byte) []byte {
|
||||
return n.Segments.Value(source)
|
||||
}
|
||||
|
||||
// NewRawHTML returns a new RawHTML node.
|
||||
func NewRawHTML() *RawHTML {
|
||||
return &RawHTML{
|
||||
|
|
|
|||
204
ast_test.go
Normal file
204
ast_test.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package goldmark_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
. "github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
"github.com/yuin/goldmark/text"
|
||||
)
|
||||
|
||||
func TestASTBlockNodeText(t *testing.T) {
|
||||
var cases = []struct {
|
||||
Name string
|
||||
Source string
|
||||
T1 string
|
||||
T2 string
|
||||
C bool
|
||||
}{
|
||||
{
|
||||
Name: "AtxHeading",
|
||||
Source: `# l1
|
||||
|
||||
a
|
||||
|
||||
# l2`,
|
||||
T1: `l1`,
|
||||
T2: `l2`,
|
||||
},
|
||||
{
|
||||
Name: "SetextHeading",
|
||||
Source: `l1
|
||||
l2
|
||||
===============
|
||||
|
||||
a
|
||||
|
||||
l3
|
||||
l4
|
||||
==============`,
|
||||
T1: `l1
|
||||
l2`,
|
||||
T2: `l3
|
||||
l4`,
|
||||
},
|
||||
{
|
||||
Name: "CodeBlock",
|
||||
Source: ` l1
|
||||
l2
|
||||
|
||||
a
|
||||
|
||||
l3
|
||||
l4`,
|
||||
T1: `l1
|
||||
l2
|
||||
`,
|
||||
T2: `l3
|
||||
l4
|
||||
`,
|
||||
},
|
||||
{
|
||||
Name: "FencedCodeBlock",
|
||||
Source: "```" + `
|
||||
l1
|
||||
l2
|
||||
` + "```" + `
|
||||
|
||||
a
|
||||
|
||||
` + "```" + `
|
||||
l3
|
||||
l4`,
|
||||
T1: `l1
|
||||
l2
|
||||
`,
|
||||
T2: `l3
|
||||
l4
|
||||
`,
|
||||
},
|
||||
{
|
||||
Name: "Blockquote",
|
||||
Source: `> l1
|
||||
> l2
|
||||
|
||||
a
|
||||
|
||||
> l3
|
||||
> l4`,
|
||||
T1: `l1
|
||||
l2`,
|
||||
T2: `l3
|
||||
l4`,
|
||||
},
|
||||
{
|
||||
Name: "List",
|
||||
Source: `- l1
|
||||
l2
|
||||
|
||||
a
|
||||
|
||||
- l3
|
||||
l4`,
|
||||
T1: `l1
|
||||
l2`,
|
||||
T2: `l3
|
||||
l4`,
|
||||
C: true,
|
||||
},
|
||||
{
|
||||
Name: "HTMLBlock",
|
||||
Source: `<div>
|
||||
l1
|
||||
l2
|
||||
</div>
|
||||
|
||||
a
|
||||
|
||||
<div>
|
||||
l3
|
||||
l4`,
|
||||
T1: `<div>
|
||||
l1
|
||||
l2
|
||||
</div>
|
||||
`,
|
||||
T2: `<div>
|
||||
l3
|
||||
l4`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
s := []byte(cs.Source)
|
||||
md := New()
|
||||
n := md.Parser().Parse(text.NewReader(s))
|
||||
c1 := n.FirstChild()
|
||||
c2 := c1.NextSibling().NextSibling()
|
||||
if cs.C {
|
||||
c1 = c1.FirstChild()
|
||||
c2 = c2.FirstChild()
|
||||
}
|
||||
if !bytes.Equal(c1.Text(s), []byte(cs.T1)) { // nolint: staticcheck
|
||||
|
||||
t.Errorf("%s unmatch: %s", cs.Name, testutil.DiffPretty(c1.Text(s), []byte(cs.T1))) // nolint: staticcheck
|
||||
|
||||
}
|
||||
if !bytes.Equal(c2.Text(s), []byte(cs.T2)) { // nolint: staticcheck
|
||||
|
||||
t.Errorf("%s(EOF) unmatch: %s", cs.Name, testutil.DiffPretty(c2.Text(s), []byte(cs.T2))) // nolint: staticcheck
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestASTInlineNodeText(t *testing.T) {
|
||||
var cases = []struct {
|
||||
Name string
|
||||
Source string
|
||||
T1 string
|
||||
}{
|
||||
{
|
||||
Name: "CodeSpan",
|
||||
Source: "`c1`",
|
||||
T1: `c1`,
|
||||
},
|
||||
{
|
||||
Name: "Emphasis",
|
||||
Source: `*c1 **c2***`,
|
||||
T1: `c1 c2`,
|
||||
},
|
||||
{
|
||||
Name: "Link",
|
||||
Source: `[label](url)`,
|
||||
T1: `label`,
|
||||
},
|
||||
{
|
||||
Name: "AutoLink",
|
||||
Source: `<http://url>`,
|
||||
T1: `http://url`,
|
||||
},
|
||||
{
|
||||
Name: "RawHTML",
|
||||
Source: `<span>c1</span>`,
|
||||
T1: `<span>`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
s := []byte(cs.Source)
|
||||
md := New()
|
||||
n := md.Parser().Parse(text.NewReader(s))
|
||||
c1 := n.FirstChild().FirstChild()
|
||||
if !bytes.Equal(c1.Text(s), []byte(cs.T1)) { // nolint: staticcheck
|
||||
t.Errorf("%s unmatch:\n%s", cs.Name, testutil.DiffPretty(c1.Text(s), []byte(cs.T1))) // nolint: staticcheck
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
package goldmark
|
||||
package goldmark_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
. "github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
)
|
||||
|
||||
type commonmarkSpecTestCase struct {
|
||||
|
|
@ -19,7 +20,7 @@ type commonmarkSpecTestCase struct {
|
|||
}
|
||||
|
||||
func TestSpec(t *testing.T) {
|
||||
bs, err := ioutil.ReadFile("_test/spec.json")
|
||||
bs, err := os.ReadFile("_test/spec.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
@ -27,30 +28,30 @@ func TestSpec(t *testing.T) {
|
|||
if err := json.Unmarshal(bs, &testCases); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cases := []testutil.MarkdownTestCase{}
|
||||
nos := testutil.ParseCliCaseArg()
|
||||
for _, c := range testCases {
|
||||
shouldAdd := len(nos) == 0
|
||||
if !shouldAdd {
|
||||
for _, no := range nos {
|
||||
if c.Example == no {
|
||||
shouldAdd = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if shouldAdd {
|
||||
cases = append(cases, testutil.MarkdownTestCase{
|
||||
No: c.Example,
|
||||
Markdown: c.Markdown,
|
||||
Expected: c.HTML,
|
||||
})
|
||||
}
|
||||
}
|
||||
markdown := New(WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
))
|
||||
for _, testCase := range testCases {
|
||||
var out bytes.Buffer
|
||||
if err := markdown.Convert([]byte(testCase.Markdown), &out); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if !bytes.Equal(bytes.TrimSpace(out.Bytes()), bytes.TrimSpace([]byte(testCase.HTML))) {
|
||||
format := `============= case %d ================
|
||||
Markdown:
|
||||
-----------
|
||||
%s
|
||||
|
||||
Expected:
|
||||
----------
|
||||
%s
|
||||
|
||||
Actual
|
||||
---------
|
||||
%s
|
||||
`
|
||||
t.Errorf(format, testCase.Example, testCase.Markdown, testCase.HTML, out.Bytes())
|
||||
}
|
||||
}
|
||||
testutil.DoTestCases(markdown, cases, t)
|
||||
}
|
||||
|
|
|
|||
157
extension/_test/definition_list.txt
Normal file
157
extension/_test/definition_list.txt
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
1
|
||||
//- - - - - - - - -//
|
||||
Apple
|
||||
: Pomaceous fruit of plants of the genus Malus in
|
||||
the family Rosaceae.
|
||||
|
||||
Orange
|
||||
: The fruit of an evergreen tree of the genus Citrus.
|
||||
//- - - - - - - - -//
|
||||
<dl>
|
||||
<dt>Apple</dt>
|
||||
<dd>Pomaceous fruit of plants of the genus Malus in
|
||||
the family Rosaceae.</dd>
|
||||
<dt>Orange</dt>
|
||||
<dd>The fruit of an evergreen tree of the genus Citrus.</dd>
|
||||
</dl>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
2
|
||||
//- - - - - - - - -//
|
||||
Apple
|
||||
: Pomaceous fruit of plants of the genus Malus in
|
||||
the family Rosaceae.
|
||||
: An American computer company.
|
||||
|
||||
Orange
|
||||
: The fruit of an evergreen tree of the genus Citrus.
|
||||
//- - - - - - - - -//
|
||||
<dl>
|
||||
<dt>Apple</dt>
|
||||
<dd>Pomaceous fruit of plants of the genus Malus in
|
||||
the family Rosaceae.</dd>
|
||||
<dd>An American computer company.</dd>
|
||||
<dt>Orange</dt>
|
||||
<dd>The fruit of an evergreen tree of the genus Citrus.</dd>
|
||||
</dl>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
3
|
||||
//- - - - - - - - -//
|
||||
Term 1
|
||||
Term 2
|
||||
: Definition a
|
||||
|
||||
Term 3
|
||||
: Definition b
|
||||
//- - - - - - - - -//
|
||||
<dl>
|
||||
<dt>Term 1</dt>
|
||||
<dt>Term 2</dt>
|
||||
<dd>Definition a</dd>
|
||||
<dt>Term 3</dt>
|
||||
<dd>Definition b</dd>
|
||||
</dl>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
4
|
||||
//- - - - - - - - -//
|
||||
Apple
|
||||
|
||||
: Pomaceous fruit of plants of the genus Malus in
|
||||
the family Rosaceae.
|
||||
|
||||
Orange
|
||||
|
||||
: The fruit of an evergreen tree of the genus Citrus.
|
||||
//- - - - - - - - -//
|
||||
<dl>
|
||||
<dt>Apple</dt>
|
||||
<dd>
|
||||
<p>Pomaceous fruit of plants of the genus Malus in
|
||||
the family Rosaceae.</p>
|
||||
</dd>
|
||||
<dt>Orange</dt>
|
||||
<dd>
|
||||
<p>The fruit of an evergreen tree of the genus Citrus.</p>
|
||||
</dd>
|
||||
</dl>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
5
|
||||
//- - - - - - - - -//
|
||||
Term 1
|
||||
|
||||
: This is a definition with two paragraphs. Lorem ipsum
|
||||
dolor sit amet, consectetuer adipiscing elit. Aliquam
|
||||
hendrerit mi posuere lectus.
|
||||
|
||||
Vestibulum enim wisi, viverra nec, fringilla in, laoreet
|
||||
vitae, risus.
|
||||
|
||||
: Second definition for term 1, also wrapped in a paragraph
|
||||
because of the blank line preceding it.
|
||||
|
||||
Term 2
|
||||
|
||||
: This definition has a code block, a blockquote and a list.
|
||||
|
||||
code block.
|
||||
|
||||
> block quote
|
||||
> on two lines.
|
||||
|
||||
1. first list item
|
||||
2. second list item
|
||||
//- - - - - - - - -//
|
||||
<dl>
|
||||
<dt>Term 1</dt>
|
||||
<dd>
|
||||
<p>This is a definition with two paragraphs. Lorem ipsum
|
||||
dolor sit amet, consectetuer adipiscing elit. Aliquam
|
||||
hendrerit mi posuere lectus.</p>
|
||||
<p>Vestibulum enim wisi, viverra nec, fringilla in, laoreet
|
||||
vitae, risus.</p>
|
||||
</dd>
|
||||
<dd>
|
||||
<p>Second definition for term 1, also wrapped in a paragraph
|
||||
because of the blank line preceding it.</p>
|
||||
</dd>
|
||||
<dt>Term 2</dt>
|
||||
<dd>
|
||||
<p>This definition has a code block, a blockquote and a list.</p>
|
||||
<pre><code>code block.
|
||||
</code></pre>
|
||||
<blockquote>
|
||||
<p>block quote
|
||||
on two lines.</p>
|
||||
</blockquote>
|
||||
<ol>
|
||||
<li>first list item</li>
|
||||
<li>second list item</li>
|
||||
</ol>
|
||||
</dd>
|
||||
</dl>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
6: Definition lists indented with tabs
|
||||
//- - - - - - - - -//
|
||||
0
|
||||
: ```
|
||||
0
|
||||
//- - - - - - - - -//
|
||||
<dl>
|
||||
<dt>0</dt>
|
||||
<dd><pre><code> 0
|
||||
</code></pre>
|
||||
</dd>
|
||||
</dl>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
91
extension/_test/footnote.txt
Normal file
91
extension/_test/footnote.txt
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
1
|
||||
//- - - - - - - - -//
|
||||
That's some text with a footnote.[^1]
|
||||
|
||||
[^1]: And that's the footnote.
|
||||
|
||||
That's the second paragraph.
|
||||
//- - - - - - - - -//
|
||||
<p>That's some text with a footnote.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
|
||||
<div class="footnotes" role="doc-endnotes">
|
||||
<hr>
|
||||
<ol>
|
||||
<li id="fn:1">
|
||||
<p>And that's the footnote.</p>
|
||||
<p>That's the second paragraph. <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
3
|
||||
//- - - - - - - - -//
|
||||
[^000]:0 [^]:
|
||||
//- - - - - - - - -//
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
4
|
||||
//- - - - - - - - -//
|
||||
This[^3] is[^1] text with footnotes[^2].
|
||||
|
||||
[^1]: Footnote one
|
||||
[^2]: Footnote two
|
||||
[^3]: Footnote three
|
||||
//- - - - - - - - -//
|
||||
<p>This<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> is<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> text with footnotes<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup>.</p>
|
||||
<div class="footnotes" role="doc-endnotes">
|
||||
<hr>
|
||||
<ol>
|
||||
<li id="fn:1">
|
||||
<p>Footnote three <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
|
||||
</li>
|
||||
<li id="fn:2">
|
||||
<p>Footnote one <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p>
|
||||
</li>
|
||||
<li id="fn:3">
|
||||
<p>Footnote two <a href="#fnref:3" class="footnote-backref" role="doc-backlink">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
5
|
||||
//- - - - - - - - -//
|
||||
test![^1]
|
||||
|
||||
[^1]: footnote
|
||||
//- - - - - - - - -//
|
||||
<p>test!<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
|
||||
<div class="footnotes" role="doc-endnotes">
|
||||
<hr>
|
||||
<ol>
|
||||
<li id="fn:1">
|
||||
<p>footnote <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
6: Multiple references to the same footnotes should have different ids
|
||||
//- - - - - - - - -//
|
||||
something[^fn:1]
|
||||
|
||||
something[^fn:1]
|
||||
|
||||
something[^fn:1]
|
||||
|
||||
[^fn:1]: footnote text
|
||||
//- - - - - - - - -//
|
||||
<p>something<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
|
||||
<p>something<sup id="fnref1:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
|
||||
<p>something<sup id="fnref2:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
|
||||
<div class="footnotes" role="doc-endnotes">
|
||||
<hr>
|
||||
<ol>
|
||||
<li id="fn:1">
|
||||
<p>footnote text <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a> <a href="#fnref1:1" class="footnote-backref" role="doc-backlink">↩︎</a> <a href="#fnref2:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
193
extension/_test/linkify.txt
Normal file
193
extension/_test/linkify.txt
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
1
|
||||
//- - - - - - - - -//
|
||||
www.commonmark.org
|
||||
//- - - - - - - - -//
|
||||
<p><a href="http://www.commonmark.org">www.commonmark.org</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
2
|
||||
//- - - - - - - - -//
|
||||
Visit www.commonmark.org/help for more information.
|
||||
//- - - - - - - - -//
|
||||
<p>Visit <a href="http://www.commonmark.org/help">www.commonmark.org/help</a> for more information.</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
3
|
||||
//- - - - - - - - -//
|
||||
www.google.com/search?q=Markup+(business)
|
||||
|
||||
www.google.com/search?q=Markup+(business)))
|
||||
|
||||
(www.google.com/search?q=Markup+(business))
|
||||
|
||||
(www.google.com/search?q=Markup+(business)
|
||||
//- - - - - - - - -//
|
||||
<p><a href="http://www.google.com/search?q=Markup+(business)">www.google.com/search?q=Markup+(business)</a></p>
|
||||
<p><a href="http://www.google.com/search?q=Markup+(business)">www.google.com/search?q=Markup+(business)</a>))</p>
|
||||
<p>(<a href="http://www.google.com/search?q=Markup+(business)">www.google.com/search?q=Markup+(business)</a>)</p>
|
||||
<p>(<a href="http://www.google.com/search?q=Markup+(business)">www.google.com/search?q=Markup+(business)</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
4
|
||||
//- - - - - - - - -//
|
||||
www.google.com/search?q=(business))+ok
|
||||
//- - - - - - - - -//
|
||||
<p><a href="http://www.google.com/search?q=(business))+ok">www.google.com/search?q=(business))+ok</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
5
|
||||
//- - - - - - - - -//
|
||||
www.google.com/search?q=commonmark&hl=en
|
||||
|
||||
www.google.com/search?q=commonmark&hl;
|
||||
//- - - - - - - - -//
|
||||
<p><a href="http://www.google.com/search?q=commonmark&hl=en">www.google.com/search?q=commonmark&hl=en</a></p>
|
||||
<p><a href="http://www.google.com/search?q=commonmark">www.google.com/search?q=commonmark</a>&hl;</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
6
|
||||
//- - - - - - - - -//
|
||||
www.commonmark.org/he<lp
|
||||
//- - - - - - - - -//
|
||||
<p><a href="http://www.commonmark.org/he">www.commonmark.org/he</a><lp</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
7
|
||||
//- - - - - - - - -//
|
||||
http://commonmark.org
|
||||
|
||||
(Visit https://encrypted.google.com/search?q=Markup+(business))
|
||||
|
||||
Anonymous FTP is available at ftp://foo.bar.baz.
|
||||
//- - - - - - - - -//
|
||||
<p><a href="http://commonmark.org">http://commonmark.org</a></p>
|
||||
<p>(Visit <a href="https://encrypted.google.com/search?q=Markup+(business)">https://encrypted.google.com/search?q=Markup+(business)</a>)</p>
|
||||
<p>Anonymous FTP is available at <a href="ftp://foo.bar.baz">ftp://foo.bar.baz</a>.</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
8
|
||||
//- - - - - - - - -//
|
||||
foo@bar.baz
|
||||
//- - - - - - - - -//
|
||||
<p><a href="mailto:foo@bar.baz">foo@bar.baz</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
9
|
||||
//- - - - - - - - -//
|
||||
hello@mail+xyz.example isn't valid, but hello+xyz@mail.example is.
|
||||
//- - - - - - - - -//
|
||||
<p>hello@mail+xyz.example isn't valid, but <a href="mailto:hello+xyz@mail.example">hello+xyz@mail.example</a> is.</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
10
|
||||
//- - - - - - - - -//
|
||||
a.b-c_d@a.b
|
||||
|
||||
a.b-c_d@a.b.
|
||||
|
||||
a.b-c_d@a.b-
|
||||
|
||||
a.b-c_d@a.b_
|
||||
//- - - - - - - - -//
|
||||
<p><a href="mailto:a.b-c_d@a.b">a.b-c_d@a.b</a></p>
|
||||
<p><a href="mailto:a.b-c_d@a.b">a.b-c_d@a.b</a>.</p>
|
||||
<p>a.b-c_d@a.b-</p>
|
||||
<p>a.b-c_d@a.b_</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
11
|
||||
//- - - - - - - - -//
|
||||
https://github.com#sun,mon
|
||||
//- - - - - - - - -//
|
||||
<p><a href="https://github.com#sun,mon">https://github.com#sun,mon</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
12
|
||||
//- - - - - - - - -//
|
||||
https://github.com/sunday's
|
||||
//- - - - - - - - -//
|
||||
<p><a href="https://github.com/sunday's">https://github.com/sunday's</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
13
|
||||
//- - - - - - - - -//
|
||||
https://github.com?q=stars:>1
|
||||
//- - - - - - - - -//
|
||||
<p><a href="https://github.com?q=stars:%3E1">https://github.com?q=stars:>1</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
14
|
||||
//- - - - - - - - -//
|
||||
[https://google.com](https://google.com)
|
||||
//- - - - - - - - -//
|
||||
<p><a href="https://google.com">https://google.com</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
15
|
||||
//- - - - - - - - -//
|
||||
This is a `git@github.com:vim/vim`
|
||||
//- - - - - - - - -//
|
||||
<p>This is a <code>git@github.com:vim/vim</code></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
16
|
||||
//- - - - - - - - -//
|
||||
https://nic.college
|
||||
//- - - - - - - - -//
|
||||
<p><a href="https://nic.college">https://nic.college</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
17
|
||||
//- - - - - - - - -//
|
||||
http://server.intranet.acme.com:1313
|
||||
//- - - - - - - - -//
|
||||
<p><a href="http://server.intranet.acme.com:1313">http://server.intranet.acme.com:1313</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
18
|
||||
//- - - - - - - - -//
|
||||
https://g.page/foo
|
||||
//- - - - - - - - -//
|
||||
<p><a href="https://g.page/foo">https://g.page/foo</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
19: Trailing punctuation (specifically, ?, !, ., ,, :, *, _, and ~) will not be considered part of the autolink
|
||||
//- - - - - - - - -//
|
||||
__http://test.com/~/a__
|
||||
__http://test.com/~/__
|
||||
__http://test.com/~__
|
||||
__http://test.com/a/~__
|
||||
//- - - - - - - - -//
|
||||
<p><strong><a href="http://test.com/~/a">http://test.com/~/a</a></strong>
|
||||
<strong><a href="http://test.com/~/">http://test.com/~/</a></strong>
|
||||
<strong><a href="http://test.com/">http://test.com/</a>~</strong>
|
||||
<strong><a href="http://test.com/a/">http://test.com/a/</a>~</strong></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
39
extension/_test/strikethrough.txt
Normal file
39
extension/_test/strikethrough.txt
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
1
|
||||
//- - - - - - - - -//
|
||||
~~Hi~~ Hello, world!
|
||||
//- - - - - - - - -//
|
||||
<p><del>Hi</del> Hello, world!</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
2
|
||||
//- - - - - - - - -//
|
||||
This ~~has a
|
||||
|
||||
new paragraph~~.
|
||||
//- - - - - - - - -//
|
||||
<p>This ~~has a</p>
|
||||
<p>new paragraph~~.</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
3
|
||||
//- - - - - - - - -//
|
||||
~Hi~ Hello, world!
|
||||
//- - - - - - - - -//
|
||||
<p><del>Hi</del> Hello, world!</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
4: Three or more tildes do not create a strikethrough
|
||||
//- - - - - - - - -//
|
||||
This will ~~~not~~~ strike.
|
||||
//- - - - - - - - -//
|
||||
<p>This will ~~~not~~~ strike.</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
5: Leading three or more tildes do not create a strikethrough, create a code block
|
||||
//- - - - - - - - -//
|
||||
~~~Hi~~~ Hello, world!
|
||||
//- - - - - - - - -//
|
||||
<pre><code class="language-Hi~~~"></code></pre>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
282
extension/_test/table.txt
Normal file
282
extension/_test/table.txt
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
1
|
||||
//- - - - - - - - -//
|
||||
| foo | bar |
|
||||
| --- | --- |
|
||||
| baz | bim |
|
||||
//- - - - - - - - -//
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>foo</th>
|
||||
<th>bar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>baz</td>
|
||||
<td>bim</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
2
|
||||
//- - - - - - - - -//
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
//- - - - - - - - -//
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center">abc</th>
|
||||
<th align="right">defghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">bar</td>
|
||||
<td align="right">baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
3
|
||||
//- - - - - - - - -//
|
||||
| f\|oo |
|
||||
| ------ |
|
||||
| b `\|` az |
|
||||
| b **\|** im |
|
||||
//- - - - - - - - -//
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>f|oo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>b <code>|</code> az</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>b <strong>|</strong> im</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
4
|
||||
//- - - - - - - - -//
|
||||
| abc | def |
|
||||
| --- | --- |
|
||||
| bar | baz |
|
||||
> bar
|
||||
//- - - - - - - - -//
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>abc</th>
|
||||
<th>def</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>bar</td>
|
||||
<td>baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<blockquote>
|
||||
<p>bar</p>
|
||||
</blockquote>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
5
|
||||
//- - - - - - - - -//
|
||||
| abc | def |
|
||||
| --- | --- |
|
||||
| bar | baz |
|
||||
bar
|
||||
|
||||
bar
|
||||
//- - - - - - - - -//
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>abc</th>
|
||||
<th>def</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>bar</td>
|
||||
<td>baz</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>bar</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>bar</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
6
|
||||
//- - - - - - - - -//
|
||||
| abc | def |
|
||||
| --- |
|
||||
| bar |
|
||||
//- - - - - - - - -//
|
||||
<p>| abc | def |
|
||||
| --- |
|
||||
| bar |</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
7
|
||||
//- - - - - - - - -//
|
||||
| abc | def |
|
||||
| --- | --- |
|
||||
| bar |
|
||||
| bar | baz | boo |
|
||||
//- - - - - - - - -//
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>abc</th>
|
||||
<th>def</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>bar</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>bar</td>
|
||||
<td>baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
8
|
||||
//- - - - - - - - -//
|
||||
| abc | def |
|
||||
| --- | --- |
|
||||
//- - - - - - - - -//
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>abc</th>
|
||||
<th>def</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
9
|
||||
//- - - - - - - - -//
|
||||
Foo|Bar
|
||||
---|---
|
||||
`Yoyo`|Dyne
|
||||
//- - - - - - - - -//
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Foo</th>
|
||||
<th>Bar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>Yoyo</code></td>
|
||||
<td>Dyne</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
10
|
||||
//- - - - - - - - -//
|
||||
foo|bar
|
||||
---|---
|
||||
`\` | second column
|
||||
//- - - - - - - - -//
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>foo</th>
|
||||
<th>bar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>\</code></td>
|
||||
<td>second column</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
11: Tables can interrupt paragraph
|
||||
//- - - - - - - - -//
|
||||
**xxx**
|
||||
| hello | hi |
|
||||
| :----: | :----:|
|
||||
//- - - - - - - - -//
|
||||
<p><strong>xxx</strong></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center">hello</th>
|
||||
<th align="center">hi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
12: A delimiter can not start with more than 3 spaces
|
||||
//- - - - - - - - -//
|
||||
Foo
|
||||
---
|
||||
//- - - - - - - - -//
|
||||
<p>Foo
|
||||
---</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
13: A delimiter can not start with more than 3 spaces(w/ tabs)
|
||||
OPTIONS: {"enableEscape": true}
|
||||
//- - - - - - - - -//
|
||||
- aaa
|
||||
|
||||
Foo
|
||||
\t\t---
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>
|
||||
<p>aaa</p>
|
||||
<p>Foo
|
||||
---</p>
|
||||
</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
51
extension/_test/tasklist.txt
Normal file
51
extension/_test/tasklist.txt
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
1
|
||||
//- - - - - - - - -//
|
||||
- [ ] foo
|
||||
- [x] bar
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li><input disabled="" type="checkbox"> foo</li>
|
||||
<li><input checked="" disabled="" type="checkbox"> bar</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
2
|
||||
//- - - - - - - - -//
|
||||
- [x] foo
|
||||
- [ ] bar
|
||||
- [x] baz
|
||||
- [ ] bim
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li><input checked="" disabled="" type="checkbox"> foo
|
||||
<ul>
|
||||
<li><input disabled="" type="checkbox"> bar</li>
|
||||
<li><input checked="" disabled="" type="checkbox"> baz</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><input disabled="" type="checkbox"> bim</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
|
||||
3
|
||||
//- - - - - - - - -//
|
||||
- test[x]=[x]
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li>test[x]=[x]</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
|
||||
4
|
||||
//- - - - - - - - -//
|
||||
+ [x] [x]
|
||||
//- - - - - - - - -//
|
||||
<ul>
|
||||
<li><input checked="" disabled="" type="checkbox"> [x]</li>
|
||||
</ul>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
143
extension/_test/typographer.txt
Normal file
143
extension/_test/typographer.txt
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
1
|
||||
//- - - - - - - - -//
|
||||
This should 'be' replaced
|
||||
//- - - - - - - - -//
|
||||
<p>This should ‘be’ replaced</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
2
|
||||
//- - - - - - - - -//
|
||||
This should "be" replaced
|
||||
//- - - - - - - - -//
|
||||
<p>This should “be” replaced</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
3
|
||||
//- - - - - - - - -//
|
||||
**--** *---* a...<< b>>
|
||||
//- - - - - - - - -//
|
||||
<p><strong>–</strong> <em>—</em> a…« b»</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
4
|
||||
//- - - - - - - - -//
|
||||
Some say '90s, others say 90's, but I can't say which is best.
|
||||
//- - - - - - - - -//
|
||||
<p>Some say ’90s, others say 90’s, but I can’t say which is best.</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
5: contractions
|
||||
//- - - - - - - - -//
|
||||
Alice's, I'm ,Don't, You'd
|
||||
|
||||
I've, I'll, You're
|
||||
|
||||
[Cat][]'s Pajamas
|
||||
|
||||
Yahoo!'s
|
||||
|
||||
[Cat]: http://example.com
|
||||
//- - - - - - - - -//
|
||||
<p>Alice’s, I’m ,Don’t, You’d</p>
|
||||
<p>I’ve, I’ll, You’re</p>
|
||||
<p><a href="http://example.com">Cat</a>’s Pajamas</p>
|
||||
<p>Yahoo!’s</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
6: "" after digits are an inch
|
||||
//- - - - - - - - -//
|
||||
My height is 5'6"".
|
||||
//- - - - - - - - -//
|
||||
<p>My height is 5'6"".</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
7: quote followed by ,.?! and spaces maybe a closer
|
||||
//- - - - - - - - -//
|
||||
reported "issue 1 (IE-only)", "issue 2", 'issue3 (FF-only)', 'issue4'
|
||||
//- - - - - - - - -//
|
||||
<p>reported “issue 1 (IE-only)”, “issue 2”, ‘issue3 (FF-only)’, ‘issue4’</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
8: handle inches in qoutes
|
||||
//- - - - - - - - -//
|
||||
"Monitor 21"" and "Monitor""
|
||||
//- - - - - - - - -//
|
||||
<p>“Monitor 21"” and “Monitor”"</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
9: Closing quotation marks within italics
|
||||
//- - - - - - - - -//
|
||||
*"At first, things were not clear."*
|
||||
//- - - - - - - - -//
|
||||
<p><em>“At first, things were not clear.”</em></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
10: Closing quotation marks within boldfacing
|
||||
//- - - - - - - - -//
|
||||
**"At first, things were not clear."**
|
||||
//- - - - - - - - -//
|
||||
<p><strong>“At first, things were not clear.”</strong></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
11: Closing quotation marks within boldfacing and italics
|
||||
//- - - - - - - - -//
|
||||
***"At first, things were not clear."***
|
||||
//- - - - - - - - -//
|
||||
<p><em><strong>“At first, things were not clear.”</strong></em></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
12: Closing quotation marks within boldfacing and italics
|
||||
//- - - - - - - - -//
|
||||
***"At first, things were not clear."***
|
||||
//- - - - - - - - -//
|
||||
<p><em><strong>“At first, things were not clear.”</strong></em></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
13: Plural possessives
|
||||
//- - - - - - - - -//
|
||||
John's dog is named Sam. The Smiths' dog is named Rover.
|
||||
//- - - - - - - - -//
|
||||
<p>John’s dog is named Sam. The Smiths’ dog is named Rover.</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
14: Links within quotation marks and parenthetical phrases
|
||||
//- - - - - - - - -//
|
||||
This is not difficult (see "[Introduction to Hugo Templating](https://gohugo.io/templates/introduction/)").
|
||||
//- - - - - - - - -//
|
||||
<p>This is not difficult (see “<a href="https://gohugo.io/templates/introduction/">Introduction to Hugo Templating</a>”).</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
15: Quotation marks within links
|
||||
//- - - - - - - - -//
|
||||
Apple's early Cairo font gave us ["moof" and the "dogcow."](https://www.macworld.com/article/2926184/we-miss-you-clarus-the-dogcow.html)
|
||||
//- - - - - - - - -//
|
||||
<p>Apple’s early Cairo font gave us <a href="https://www.macworld.com/article/2926184/we-miss-you-clarus-the-dogcow.html">“moof” and the “dogcow.”</a></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
16: Single closing quotation marks with slang/informalities
|
||||
//- - - - - - - - -//
|
||||
"I'm not doin' that," Bill said with emphasis.
|
||||
//- - - - - - - - -//
|
||||
<p>“I’m not doin’ that,” Bill said with emphasis.</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
17: Closing single quotation marks in quotations-within-quotations
|
||||
//- - - - - - - - -//
|
||||
Janet said, "When everything is 'breaking news,' nothing is 'breaking news.'"
|
||||
//- - - - - - - - -//
|
||||
<p>Janet said, “When everything is ‘breaking news,’ nothing is ‘breaking news.’”</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
18: Opening single quotation marks for abbreviations
|
||||
//- - - - - - - - -//
|
||||
We're talking about the internet --- 'net for short. Let's rock 'n roll!
|
||||
//- - - - - - - - -//
|
||||
<p>We’re talking about the internet — ’net for short. Let’s rock ’n roll!</p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
||||
19: Quotes in alt text
|
||||
//- - - - - - - - -//
|
||||

|
||||
//- - - - - - - - -//
|
||||
<p><img src="https://example.com/image.jpg" alt="Nice & day, isn’t it?"></p>
|
||||
//= = = = = = = = = = = = = = = = = = = = = = = =//
|
||||
|
|
@ -2,6 +2,7 @@ package ast
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
|
|
@ -9,13 +10,17 @@ import (
|
|||
// (PHP Markdown Extra) text.
|
||||
type FootnoteLink struct {
|
||||
gast.BaseInline
|
||||
Index int
|
||||
Index int
|
||||
RefCount int
|
||||
RefIndex int
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *FootnoteLink) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
m["Index"] = fmt.Sprintf("%v", n.Index)
|
||||
m["RefCount"] = fmt.Sprintf("%v", n.RefCount)
|
||||
m["RefIndex"] = fmt.Sprintf("%v", n.RefIndex)
|
||||
gast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
|
|
@ -30,7 +35,44 @@ func (n *FootnoteLink) Kind() gast.NodeKind {
|
|||
// NewFootnoteLink returns a new FootnoteLink node.
|
||||
func NewFootnoteLink(index int) *FootnoteLink {
|
||||
return &FootnoteLink{
|
||||
Index: index,
|
||||
Index: index,
|
||||
RefCount: 0,
|
||||
RefIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// A FootnoteBacklink struct represents a link to a footnote of Markdown
|
||||
// (PHP Markdown Extra) text.
|
||||
type FootnoteBacklink struct {
|
||||
gast.BaseInline
|
||||
Index int
|
||||
RefCount int
|
||||
RefIndex int
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *FootnoteBacklink) Dump(source []byte, level int) {
|
||||
m := map[string]string{}
|
||||
m["Index"] = fmt.Sprintf("%v", n.Index)
|
||||
m["RefCount"] = fmt.Sprintf("%v", n.RefCount)
|
||||
m["RefIndex"] = fmt.Sprintf("%v", n.RefIndex)
|
||||
gast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
// KindFootnoteBacklink is a NodeKind of the FootnoteBacklink node.
|
||||
var KindFootnoteBacklink = gast.NewNodeKind("FootnoteBacklink")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *FootnoteBacklink) Kind() gast.NodeKind {
|
||||
return KindFootnoteBacklink
|
||||
}
|
||||
|
||||
// NewFootnoteBacklink returns a new FootnoteBacklink node.
|
||||
func NewFootnoteBacklink(index int) *FootnoteBacklink {
|
||||
return &FootnoteBacklink{
|
||||
Index: index,
|
||||
RefCount: 0,
|
||||
RefIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +86,10 @@ type Footnote struct {
|
|||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *Footnote) Dump(source []byte, level int) {
|
||||
gast.DumpHelper(n, source, level, nil, nil)
|
||||
m := map[string]string{}
|
||||
m["Index"] = fmt.Sprintf("%v", n.Index)
|
||||
m["Ref"] = string(n.Ref)
|
||||
gast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
// KindFootnote is a NodeKind of the Footnote node.
|
||||
|
|
@ -58,7 +103,8 @@ func (n *Footnote) Kind() gast.NodeKind {
|
|||
// NewFootnote returns a new Footnote node.
|
||||
func NewFootnote(ref []byte) *Footnote {
|
||||
return &Footnote{
|
||||
Ref: ref,
|
||||
Ref: ref,
|
||||
Index: -1,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,11 +112,14 @@ func NewFootnote(ref []byte) *Footnote {
|
|||
// (PHP Markdown Extra) text.
|
||||
type FootnoteList struct {
|
||||
gast.BaseBlock
|
||||
Count int
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *FootnoteList) Dump(source []byte, level int) {
|
||||
gast.DumpHelper(n, source, level, nil, nil)
|
||||
m := map[string]string{}
|
||||
m["Count"] = fmt.Sprintf("%v", n.Count)
|
||||
gast.DumpHelper(n, source, level, m, nil)
|
||||
}
|
||||
|
||||
// KindFootnoteList is a NodeKind of the FootnoteList node.
|
||||
|
|
@ -83,5 +132,7 @@ func (n *FootnoteList) Kind() gast.NodeKind {
|
|||
|
||||
// NewFootnoteList returns a new FootnoteList node.
|
||||
func NewFootnoteList() *FootnoteList {
|
||||
return &FootnoteList{}
|
||||
return &FootnoteList{
|
||||
Count: 0,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ package ast
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
"strings"
|
||||
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
// Alignment is a text alignment of table cells.
|
||||
|
|
@ -45,7 +46,7 @@ type Table struct {
|
|||
Alignments []Alignment
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump
|
||||
// Dump implements Node.Dump.
|
||||
func (n *Table) Dump(source []byte, level int) {
|
||||
gast.DumpHelper(n, source, level, nil, func(level int) {
|
||||
indent := strings.Repeat(" ", level)
|
||||
|
|
@ -97,12 +98,13 @@ func (n *TableRow) Kind() gast.NodeKind {
|
|||
|
||||
// NewTableRow returns a new TableRow node.
|
||||
func NewTableRow(alignments []Alignment) *TableRow {
|
||||
return &TableRow{}
|
||||
return &TableRow{Alignments: alignments}
|
||||
}
|
||||
|
||||
// A TableHeader struct represents a table header of Markdown(GFM) text.
|
||||
type TableHeader struct {
|
||||
*TableRow
|
||||
gast.BaseBlock
|
||||
Alignments []Alignment
|
||||
}
|
||||
|
||||
// KindTableHeader is a NodeKind of the TableHeader node.
|
||||
|
|
@ -113,9 +115,20 @@ func (n *TableHeader) Kind() gast.NodeKind {
|
|||
return KindTableHeader
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *TableHeader) Dump(source []byte, level int) {
|
||||
gast.DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
||||
// NewTableHeader returns a new TableHeader node.
|
||||
func NewTableHeader(row *TableRow) *TableHeader {
|
||||
return &TableHeader{row}
|
||||
n := &TableHeader{}
|
||||
for c := row.FirstChild(); c != nil; {
|
||||
next := c.NextSibling()
|
||||
n.AppendChild(n, c)
|
||||
c = next
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// A TableCell struct represents a table cell of a Markdown(GFM) text.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ type TaskCheckBox struct {
|
|||
IsChecked bool
|
||||
}
|
||||
|
||||
// Dump impelemtns Node.Dump.
|
||||
// Dump implements Node.Dump.
|
||||
func (n *TaskCheckBox) Dump(source []byte, level int) {
|
||||
m := map[string]string{
|
||||
"Checked": fmt.Sprintf("%v", n.IsChecked),
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
package ast
|
||||
|
||||
import (
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
// A TypographicText struct represents text that
|
||||
// typographic text replaces certain punctuations.
|
||||
type TypographicText struct {
|
||||
gast.BaseInline
|
||||
Value []byte
|
||||
}
|
||||
|
||||
// Dump implements Node.Dump.
|
||||
func (n *TypographicText) Dump(source []byte, level int) {
|
||||
gast.DumpHelper(n, source, level, nil, nil)
|
||||
}
|
||||
|
||||
// KindTypographicText is a NodeKind of the TypographicText node.
|
||||
var KindTypographicText = gast.NewNodeKind("TypographicText")
|
||||
|
||||
// Kind implements Node.Kind.
|
||||
func (n *TypographicText) Kind() gast.NodeKind {
|
||||
return KindTypographicText
|
||||
}
|
||||
|
||||
// NewTypographicText returns a new TypographicText node.
|
||||
func NewTypographicText(value []byte) *TypographicText {
|
||||
return &TypographicText{
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
123
extension/ast_test.go
Normal file
123
extension/ast_test.go
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
"github.com/yuin/goldmark/text"
|
||||
)
|
||||
|
||||
func TestASTBlockNodeText(t *testing.T) {
|
||||
var cases = []struct {
|
||||
Name string
|
||||
Source string
|
||||
T1 string
|
||||
T2 string
|
||||
C bool
|
||||
}{
|
||||
{
|
||||
Name: "DefinitionList",
|
||||
Source: `c1
|
||||
: c2
|
||||
c3
|
||||
|
||||
a
|
||||
|
||||
c4
|
||||
: c5
|
||||
c6`,
|
||||
T1: `c1c2
|
||||
c3`,
|
||||
T2: `c4c5
|
||||
c6`,
|
||||
},
|
||||
{
|
||||
Name: "Table",
|
||||
Source: `| h1 | h2 |
|
||||
| -- | -- |
|
||||
| c1 | c2 |
|
||||
|
||||
a
|
||||
|
||||
|
||||
| h3 | h4 |
|
||||
| -- | -- |
|
||||
| c3 | c4 |`,
|
||||
|
||||
T1: `h1h2c1c2`,
|
||||
T2: `h3h4c3c4`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
s := []byte(cs.Source)
|
||||
md := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
DefinitionList,
|
||||
Table,
|
||||
),
|
||||
)
|
||||
n := md.Parser().Parse(text.NewReader(s))
|
||||
c1 := n.FirstChild()
|
||||
c2 := c1.NextSibling().NextSibling()
|
||||
if cs.C {
|
||||
c1 = c1.FirstChild()
|
||||
c2 = c2.FirstChild()
|
||||
}
|
||||
if !bytes.Equal(c1.Text(s), []byte(cs.T1)) { // nolint: staticcheck
|
||||
|
||||
t.Errorf("%s unmatch:\n%s", cs.Name, testutil.DiffPretty(c1.Text(s), []byte(cs.T1))) // nolint: staticcheck
|
||||
|
||||
}
|
||||
if !bytes.Equal(c2.Text(s), []byte(cs.T2)) { // nolint: staticcheck
|
||||
|
||||
t.Errorf("%s(EOF) unmatch: %s", cs.Name, testutil.DiffPretty(c2.Text(s), []byte(cs.T2))) // nolint: staticcheck
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestASTInlineNodeText(t *testing.T) {
|
||||
var cases = []struct {
|
||||
Name string
|
||||
Source string
|
||||
T1 string
|
||||
}{
|
||||
{
|
||||
Name: "Strikethrough",
|
||||
Source: `~c1 *c2*~`,
|
||||
T1: `c1 c2`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cs := range cases {
|
||||
t.Run(cs.Name, func(t *testing.T) {
|
||||
s := []byte(cs.Source)
|
||||
md := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
Strikethrough,
|
||||
),
|
||||
)
|
||||
n := md.Parser().Parse(text.NewReader(s))
|
||||
c1 := n.FirstChild().FirstChild()
|
||||
if !bytes.Equal(c1.Text(s), []byte(cs.T1)) { // nolint: staticcheck
|
||||
|
||||
t.Errorf("%s unmatch:\n%s", cs.Name, testutil.DiffPretty(c1.Text(s), []byte(cs.T1))) // nolint: staticcheck
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
72
extension/cjk.go
Normal file
72
extension/cjk.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
)
|
||||
|
||||
// A CJKOption sets options for CJK support mostly for HTML based renderers.
|
||||
type CJKOption func(*cjk)
|
||||
|
||||
// A EastAsianLineBreaks is a style of east asian line breaks.
|
||||
type EastAsianLineBreaks int
|
||||
|
||||
const (
|
||||
//EastAsianLineBreaksNone renders line breaks as it is.
|
||||
EastAsianLineBreaksNone EastAsianLineBreaks = iota
|
||||
// EastAsianLineBreaksSimple is a style where soft line breaks are ignored
|
||||
// if both sides of the break are east asian wide characters.
|
||||
EastAsianLineBreaksSimple
|
||||
// EastAsianLineBreaksCSS3Draft is a style where soft line breaks are ignored
|
||||
// even if only one side of the break is an east asian wide character.
|
||||
EastAsianLineBreaksCSS3Draft
|
||||
)
|
||||
|
||||
// WithEastAsianLineBreaks is a functional option that indicates whether softline breaks
|
||||
// between east asian wide characters should be ignored.
|
||||
// style defauts to [EastAsianLineBreaksSimple] .
|
||||
func WithEastAsianLineBreaks(style ...EastAsianLineBreaks) CJKOption {
|
||||
return func(c *cjk) {
|
||||
if len(style) == 0 {
|
||||
c.EastAsianLineBreaks = EastAsianLineBreaksSimple
|
||||
return
|
||||
}
|
||||
c.EastAsianLineBreaks = style[0]
|
||||
}
|
||||
}
|
||||
|
||||
// WithEscapedSpace is a functional option that indicates that a '\' escaped half-space(0x20) should not be rendered.
|
||||
func WithEscapedSpace() CJKOption {
|
||||
return func(c *cjk) {
|
||||
c.EscapedSpace = true
|
||||
}
|
||||
}
|
||||
|
||||
type cjk struct {
|
||||
EastAsianLineBreaks EastAsianLineBreaks
|
||||
EscapedSpace bool
|
||||
}
|
||||
|
||||
// CJK is a goldmark extension that provides functionalities for CJK languages.
|
||||
var CJK = NewCJK(WithEastAsianLineBreaks(), WithEscapedSpace())
|
||||
|
||||
// NewCJK returns a new extension with given options.
|
||||
func NewCJK(opts ...CJKOption) goldmark.Extender {
|
||||
e := &cjk{
|
||||
EastAsianLineBreaks: EastAsianLineBreaksNone,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *cjk) Extend(m goldmark.Markdown) {
|
||||
m.Renderer().AddOptions(html.WithEastAsianLineBreaks(
|
||||
html.EastAsianLineBreaks(e.EastAsianLineBreaks)))
|
||||
if e.EscapedSpace {
|
||||
m.Renderer().AddOptions(html.WithWriter(html.NewWriter(html.WithEscapedSpace())))
|
||||
m.Parser().AddOptions(parser.WithEscapedSpace())
|
||||
}
|
||||
}
|
||||
269
extension/cjk_test.go
Normal file
269
extension/cjk_test.go
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
)
|
||||
|
||||
func TestEscapedSpace(t *testing.T) {
|
||||
markdown := goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
))
|
||||
no := 1
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Without spaces around an emphasis started with east asian punctuations, it is not interpreted as an emphasis(as defined in CommonMark spec)",
|
||||
Markdown: "太郎は**「こんにちわ」**と言った\nんです",
|
||||
Expected: "<p>太郎は**「こんにちわ」**と言った\nんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
no = 2
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "With spaces around an emphasis started with east asian punctuations, it is interpreted as an emphasis(but remains unnecessary spaces)",
|
||||
Markdown: "太郎は **「こんにちわ」** と言った\nんです",
|
||||
Expected: "<p>太郎は <strong>「こんにちわ」</strong> と言った\nんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
// Enables EscapedSpace
|
||||
markdown = goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(NewCJK(WithEscapedSpace())),
|
||||
)
|
||||
|
||||
no = 3
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "With spaces around an emphasis started with east asian punctuations,it is interpreted as an emphasis",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nんです",
|
||||
Expected: "<p>太郎は<strong>「こんにちわ」</strong>と言った\nんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
// ' ' triggers Linkify extension inline parser.
|
||||
// Escaped spaces should not trigger the inline parser.
|
||||
|
||||
markdown = goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewCJK(WithEscapedSpace()),
|
||||
Linkify,
|
||||
),
|
||||
)
|
||||
|
||||
no = 4
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Escaped space and linkfy extension",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nんです",
|
||||
Expected: "<p>太郎は<strong>「こんにちわ」</strong>と言った\nんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
||||
func TestEastAsianLineBreaks(t *testing.T) {
|
||||
markdown := goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
))
|
||||
no := 1
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks are rendered as a newline, so some asian users will see it as an unnecessary space",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言った\nんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
// Enables EastAsianLineBreaks
|
||||
|
||||
markdown = goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(NewCJK(WithEastAsianLineBreaks())),
|
||||
)
|
||||
|
||||
no = 2
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between east asian wide characters are ignored",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言ったんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
no = 3
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between western characters are rendered as a newline",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言ったa\nbんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言ったa\nbんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
no = 4
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between a western character and an east asian wide character are rendered as a newline",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言ったa\nんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言ったa\nんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
no = 5
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between an east asian wide character and a western character are rendered as a newline",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nbんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言った\nbんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
// WithHardWraps take precedence over WithEastAsianLineBreaks
|
||||
markdown = goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithHardWraps(),
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(NewCJK(WithEastAsianLineBreaks())),
|
||||
)
|
||||
no = 6
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "WithHardWraps take precedence over WithEastAsianLineBreaks",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言った<br />\nんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
// Tests with EastAsianLineBreaksStyleSimple
|
||||
markdown = goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewCJK(WithEastAsianLineBreaks()),
|
||||
Linkify,
|
||||
),
|
||||
)
|
||||
no = 7
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "WithEastAsianLineBreaks and linkfy extension",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\r\nんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言ったんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
no = 8
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between east asian wide characters or punctuations are ignored",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と、\r\n言った\r\nんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と、言ったんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
no = 9
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between an east asian wide character and a western character are ignored",
|
||||
Markdown: "私はプログラマーです。\n東京の会社に勤めています。\nGoでWebアプリケーションを開発しています。",
|
||||
Expected: "<p>私はプログラマーです。東京の会社に勤めています。\nGoでWebアプリケーションを開発しています。</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
// Tests with EastAsianLineBreaksCSS3Draft
|
||||
markdown = goldmark.New(goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewCJK(WithEastAsianLineBreaks(EastAsianLineBreaksCSS3Draft)),
|
||||
),
|
||||
)
|
||||
no = 10
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between a western character and an east asian wide character are ignored",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言ったa\nんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言ったaんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
no = 11
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between an east asian wide character and a western character are ignored",
|
||||
Markdown: "太郎は\\ **「こんにちわ」**\\ と言った\nbんです",
|
||||
Expected: "<p>太郎は\\ <strong>「こんにちわ」</strong>\\ と言ったbんです</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
no = 12
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: no,
|
||||
Description: "Soft line breaks between an east asian wide character and a western character are ignored",
|
||||
Markdown: "私はプログラマーです。\n東京の会社に勤めています。\nGoでWebアプリケーションを開発しています。",
|
||||
Expected: "<p>私はプログラマーです。東京の会社に勤めています。GoでWebアプリケーションを開発しています。</p>",
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -22,13 +22,18 @@ func NewDefinitionListParser() parser.BlockParser {
|
|||
return defaultDefinitionListParser
|
||||
}
|
||||
|
||||
func (b *definitionListParser) Trigger() []byte {
|
||||
return []byte{':'}
|
||||
}
|
||||
|
||||
func (b *definitionListParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
|
||||
if _, ok := parent.(*ast.DefinitionList); ok {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
line, _ := reader.PeekLine()
|
||||
pos := pc.BlockOffset()
|
||||
if line[pos] != ':' {
|
||||
indent := pc.BlockIndent()
|
||||
if pos < 0 || line[pos] != ':' || indent != 0 {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +50,7 @@ func (b *definitionListParser) Open(parent gast.Node, reader text.Reader, pc par
|
|||
|
||||
para, lastIsParagraph := last.(*gast.Paragraph)
|
||||
var list *ast.DefinitionList
|
||||
status := parser.HasChildren
|
||||
var ok bool
|
||||
if lastIsParagraph {
|
||||
list, ok = last.PreviousSibling().(*ast.DefinitionList)
|
||||
|
|
@ -53,6 +59,7 @@ func (b *definitionListParser) Open(parent gast.Node, reader text.Reader, pc par
|
|||
list.TemporaryParagraph = para
|
||||
} else { // is first item
|
||||
list = ast.NewDefinitionList(w, para)
|
||||
status |= parser.RequireParagraph
|
||||
}
|
||||
} else if list, ok = last.(*ast.DefinitionList); ok { // multiple description
|
||||
list.Offset = w
|
||||
|
|
@ -61,7 +68,7 @@ func (b *definitionListParser) Open(parent gast.Node, reader text.Reader, pc par
|
|||
return nil, parser.NoChildren
|
||||
}
|
||||
|
||||
return list, parser.HasChildren
|
||||
return list, status
|
||||
}
|
||||
|
||||
func (b *definitionListParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
|
||||
|
|
@ -102,13 +109,22 @@ func NewDefinitionDescriptionParser() parser.BlockParser {
|
|||
return defaultDefinitionDescriptionParser
|
||||
}
|
||||
|
||||
func (b *definitionDescriptionParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
|
||||
func (b *definitionDescriptionParser) Trigger() []byte {
|
||||
return []byte{':'}
|
||||
}
|
||||
|
||||
func (b *definitionDescriptionParser) Open(
|
||||
parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
|
||||
line, _ := reader.PeekLine()
|
||||
pos := pc.BlockOffset()
|
||||
if line[pos] != ':' {
|
||||
indent := pc.BlockIndent()
|
||||
if pos < 0 || line[pos] != ':' || indent != 0 {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
list, _ := parent.(*ast.DefinitionList)
|
||||
if list == nil {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
para := list.TemporaryParagraph
|
||||
list.TemporaryParagraph = nil
|
||||
if para != nil {
|
||||
|
|
@ -123,7 +139,7 @@ func (b *definitionDescriptionParser) Open(parent gast.Node, reader text.Reader,
|
|||
para.Parent().RemoveChild(para.Parent(), para)
|
||||
}
|
||||
cpos, padding := util.IndentPosition(line[pos+1:], pos+1, list.Offset-pos-1)
|
||||
reader.AdvanceAndSetPadding(cpos, padding)
|
||||
reader.AdvanceAndSetPadding(cpos+1, padding)
|
||||
|
||||
return ast.NewDefinitionDescription(), parser.HasChildren
|
||||
}
|
||||
|
|
@ -181,34 +197,62 @@ func (r *DefinitionListHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFunc
|
|||
reg.Register(ast.KindDefinitionDescription, r.renderDefinitionDescription)
|
||||
}
|
||||
|
||||
func (r *DefinitionListHTMLRenderer) renderDefinitionList(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
w.WriteString("<dl>\n")
|
||||
} else {
|
||||
w.WriteString("</dl>\n")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
// DefinitionListAttributeFilter defines attribute names which dl elements can have.
|
||||
var DefinitionListAttributeFilter = html.GlobalAttributeFilter
|
||||
|
||||
func (r *DefinitionListHTMLRenderer) renderDefinitionTerm(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
func (r *DefinitionListHTMLRenderer) renderDefinitionList(
|
||||
w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
w.WriteString("<dt>")
|
||||
} else {
|
||||
w.WriteString("</dt>\n")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *DefinitionListHTMLRenderer) renderDefinitionDescription(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
n := node.(*ast.DefinitionDescription)
|
||||
if n.IsTight {
|
||||
w.WriteString("<dd>")
|
||||
if n.Attributes() != nil {
|
||||
_, _ = w.WriteString("<dl")
|
||||
html.RenderAttributes(w, n, DefinitionListAttributeFilter)
|
||||
_, _ = w.WriteString(">\n")
|
||||
} else {
|
||||
w.WriteString("<dd>\n")
|
||||
_, _ = w.WriteString("<dl>\n")
|
||||
}
|
||||
} else {
|
||||
w.WriteString("</dd>\n")
|
||||
_, _ = w.WriteString("</dl>\n")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// DefinitionTermAttributeFilter defines attribute names which dd elements can have.
|
||||
var DefinitionTermAttributeFilter = html.GlobalAttributeFilter
|
||||
|
||||
func (r *DefinitionListHTMLRenderer) renderDefinitionTerm(
|
||||
w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
if n.Attributes() != nil {
|
||||
_, _ = w.WriteString("<dt")
|
||||
html.RenderAttributes(w, n, DefinitionTermAttributeFilter)
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
_, _ = w.WriteString("<dt>")
|
||||
}
|
||||
} else {
|
||||
_, _ = w.WriteString("</dt>\n")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// DefinitionDescriptionAttributeFilter defines attribute names which dd elements can have.
|
||||
var DefinitionDescriptionAttributeFilter = html.GlobalAttributeFilter
|
||||
|
||||
func (r *DefinitionListHTMLRenderer) renderDefinitionDescription(
|
||||
w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
n := node.(*ast.DefinitionDescription)
|
||||
_, _ = w.WriteString("<dd")
|
||||
if n.Attributes() != nil {
|
||||
html.RenderAttributes(w, n, DefinitionDescriptionAttributeFilter)
|
||||
}
|
||||
if n.IsTight {
|
||||
_, _ = w.WriteString(">")
|
||||
} else {
|
||||
_, _ = w.WriteString(">\n")
|
||||
}
|
||||
} else {
|
||||
_, _ = w.WriteString("</dd>\n")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
|
|
|||
21
extension/definition_list_test.go
Normal file
21
extension/definition_list_test.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
)
|
||||
|
||||
func TestDefinitionList(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
DefinitionList,
|
||||
),
|
||||
)
|
||||
testutil.DoTestCaseFile(markdown, "_test/definition_list.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
|
|
@ -2,6 +2,9 @@ package extension
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/extension/ast"
|
||||
|
|
@ -10,10 +13,10 @@ import (
|
|||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var footnoteListKey = parser.NewContextKey()
|
||||
var footnoteLinkListKey = parser.NewContextKey()
|
||||
|
||||
type footnoteBlockParser struct {
|
||||
}
|
||||
|
|
@ -26,10 +29,14 @@ func NewFootnoteBlockParser() parser.BlockParser {
|
|||
return defaultFootnoteBlockParser
|
||||
}
|
||||
|
||||
func (b *footnoteBlockParser) Trigger() []byte {
|
||||
return []byte{'['}
|
||||
}
|
||||
|
||||
func (b *footnoteBlockParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
|
||||
line, segment := reader.PeekLine()
|
||||
pos := pc.BlockOffset()
|
||||
if line[pos] != '[' {
|
||||
if pos < 0 || line[pos] != '[' {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
pos++
|
||||
|
|
@ -37,25 +44,30 @@ func (b *footnoteBlockParser) Open(parent gast.Node, reader text.Reader, pc pars
|
|||
return nil, parser.NoChildren
|
||||
}
|
||||
open := pos + 1
|
||||
closes := -1
|
||||
closure := util.FindClosure(line[pos+1:], '[', ']', false, false)
|
||||
var closes int
|
||||
closure := util.FindClosure(line[pos+1:], '[', ']', false, false) //nolint:staticcheck
|
||||
closes = pos + 1 + closure
|
||||
next := closes + 1
|
||||
if closure > -1 {
|
||||
closes = pos + 1 + closure
|
||||
next := closes + 1
|
||||
if next >= len(line) || line[next] != ':' {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
} else {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
label := reader.Value(text.NewSegment(segment.Start+open, segment.Start+closes))
|
||||
padding := segment.Padding
|
||||
label := reader.Value(text.NewSegment(segment.Start+open-padding, segment.Start+closes-padding))
|
||||
if util.IsBlank(label) {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
pos = pos + 2 + closes - open + 2
|
||||
childpos, padding := util.IndentPosition(line[pos:], pos, 1)
|
||||
reader.AdvanceAndSetPadding(pos+childpos, padding)
|
||||
item := ast.NewFootnote(label)
|
||||
|
||||
pos = next + 1 - padding
|
||||
if pos >= len(line) {
|
||||
reader.Advance(pos)
|
||||
return item, parser.NoChildren
|
||||
}
|
||||
reader.AdvanceAndSetPadding(pos, padding)
|
||||
return item, parser.HasChildren
|
||||
}
|
||||
|
||||
|
|
@ -79,16 +91,9 @@ func (b *footnoteBlockParser) Close(node gast.Node, reader text.Reader, pc parse
|
|||
} else {
|
||||
list = ast.NewFootnoteList()
|
||||
pc.Set(footnoteListKey, list)
|
||||
var root gast.Node
|
||||
for n := node; n != nil; n = n.Parent() {
|
||||
root = n
|
||||
}
|
||||
root.AppendChild(root, list)
|
||||
node.Parent().InsertBefore(node.Parent(), node, list)
|
||||
}
|
||||
node.Parent().RemoveChild(node.Parent(), node)
|
||||
n := node.(*ast.Footnote)
|
||||
index := list.ChildCount() + 1
|
||||
n.Index = index
|
||||
list.AppendChild(list, node)
|
||||
}
|
||||
|
||||
|
|
@ -112,12 +117,17 @@ func NewFootnoteParser() parser.InlineParser {
|
|||
}
|
||||
|
||||
func (s *footnoteParser) Trigger() []byte {
|
||||
return []byte{'['}
|
||||
// footnote syntax probably conflict with the image syntax.
|
||||
// So we need trigger this parser with '!'.
|
||||
return []byte{'!', '['}
|
||||
}
|
||||
|
||||
func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
|
||||
line, segment := block.PeekLine()
|
||||
pos := 1
|
||||
if len(line) > 0 && line[0] == '!' {
|
||||
pos++
|
||||
}
|
||||
if pos >= len(line) || line[pos] != '^' {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -126,7 +136,7 @@ func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Co
|
|||
return nil
|
||||
}
|
||||
open := pos
|
||||
closure := util.FindClosure(line[pos:], '[', ']', false, false)
|
||||
closure := util.FindClosure(line[pos:], '[', ']', false, false) //nolint:staticcheck
|
||||
if closure < 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -145,6 +155,10 @@ func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Co
|
|||
for def := list.FirstChild(); def != nil; def = def.NextSibling() {
|
||||
d := def.(*ast.Footnote)
|
||||
if bytes.Equal(d.Ref, value) {
|
||||
if d.Index < 0 {
|
||||
list.Count++
|
||||
d.Index = list.Count
|
||||
}
|
||||
index = d.Index
|
||||
break
|
||||
}
|
||||
|
|
@ -153,7 +167,20 @@ func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Co
|
|||
return nil
|
||||
}
|
||||
|
||||
return ast.NewFootnoteLink(index)
|
||||
fnlink := ast.NewFootnoteLink(index)
|
||||
var fnlist []*ast.FootnoteLink
|
||||
if tmp := pc.Get(footnoteLinkListKey); tmp != nil {
|
||||
fnlist = tmp.([]*ast.FootnoteLink)
|
||||
} else {
|
||||
fnlist = []*ast.FootnoteLink{}
|
||||
pc.Set(footnoteLinkListKey, fnlist)
|
||||
}
|
||||
pc.Set(footnoteLinkListKey, append(fnlist, fnlink))
|
||||
if line[0] == '!' {
|
||||
parent.AppendChild(parent, gast.NewTextSegment(text.NewSegment(segment.Start, segment.Start+1)))
|
||||
}
|
||||
|
||||
return fnlink
|
||||
}
|
||||
|
||||
type footnoteASTTransformer struct {
|
||||
|
|
@ -169,29 +196,323 @@ func NewFootnoteASTTransformer() parser.ASTTransformer {
|
|||
|
||||
func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) {
|
||||
var list *ast.FootnoteList
|
||||
if tlist := pc.Get(footnoteListKey); tlist != nil {
|
||||
list = tlist.(*ast.FootnoteList)
|
||||
list.Parent().RemoveChild(list.Parent(), list)
|
||||
} else {
|
||||
var fnlist []*ast.FootnoteLink
|
||||
if tmp := pc.Get(footnoteListKey); tmp != nil {
|
||||
list = tmp.(*ast.FootnoteList)
|
||||
}
|
||||
if tmp := pc.Get(footnoteLinkListKey); tmp != nil {
|
||||
fnlist = tmp.([]*ast.FootnoteLink)
|
||||
}
|
||||
|
||||
pc.Set(footnoteListKey, nil)
|
||||
pc.Set(footnoteLinkListKey, nil)
|
||||
|
||||
if list == nil {
|
||||
return
|
||||
}
|
||||
pc.Set(footnoteListKey, nil)
|
||||
|
||||
counter := map[int]int{}
|
||||
if fnlist != nil {
|
||||
for _, fnlink := range fnlist {
|
||||
if fnlink.Index >= 0 {
|
||||
counter[fnlink.Index]++
|
||||
}
|
||||
}
|
||||
refCounter := map[int]int{}
|
||||
for _, fnlink := range fnlist {
|
||||
fnlink.RefCount = counter[fnlink.Index]
|
||||
if _, ok := refCounter[fnlink.Index]; !ok {
|
||||
refCounter[fnlink.Index] = 0
|
||||
}
|
||||
fnlink.RefIndex = refCounter[fnlink.Index]
|
||||
refCounter[fnlink.Index]++
|
||||
}
|
||||
}
|
||||
for footnote := list.FirstChild(); footnote != nil; {
|
||||
var container gast.Node = footnote
|
||||
next := footnote.NextSibling()
|
||||
if fc := container.LastChild(); fc != nil && gast.IsParagraph(fc) {
|
||||
container = fc
|
||||
}
|
||||
fn := footnote.(*ast.Footnote)
|
||||
index := fn.Index
|
||||
if index < 0 {
|
||||
list.RemoveChild(list, footnote)
|
||||
} else {
|
||||
refCount := counter[index]
|
||||
backLink := ast.NewFootnoteBacklink(index)
|
||||
backLink.RefCount = refCount
|
||||
backLink.RefIndex = 0
|
||||
container.AppendChild(container, backLink)
|
||||
if refCount > 1 {
|
||||
for i := 1; i < refCount; i++ {
|
||||
backLink := ast.NewFootnoteBacklink(index)
|
||||
backLink.RefCount = refCount
|
||||
backLink.RefIndex = i
|
||||
container.AppendChild(container, backLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
footnote = next
|
||||
}
|
||||
list.SortChildren(func(n1, n2 gast.Node) int {
|
||||
if n1.(*ast.Footnote).Index < n2.(*ast.Footnote).Index {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
})
|
||||
if list.Count <= 0 {
|
||||
list.Parent().RemoveChild(list.Parent(), list)
|
||||
return
|
||||
}
|
||||
|
||||
node.AppendChild(node, list)
|
||||
}
|
||||
|
||||
// FootnoteConfig holds configuration values for the footnote extension.
|
||||
//
|
||||
// Link* and Backlink* configurations have some variables:
|
||||
// Occurrences of “^^” in the string will be replaced by the
|
||||
// corresponding footnote number in the HTML output.
|
||||
// Occurrences of “%%” will be replaced by a number for the
|
||||
// reference (footnotes can have multiple references).
|
||||
type FootnoteConfig struct {
|
||||
html.Config
|
||||
|
||||
// IDPrefix is a prefix for the id attributes generated by footnotes.
|
||||
IDPrefix []byte
|
||||
|
||||
// IDPrefix is a function that determines the id attribute for given Node.
|
||||
IDPrefixFunction func(gast.Node) []byte
|
||||
|
||||
// LinkTitle is an optional title attribute for footnote links.
|
||||
LinkTitle []byte
|
||||
|
||||
// BacklinkTitle is an optional title attribute for footnote backlinks.
|
||||
BacklinkTitle []byte
|
||||
|
||||
// LinkClass is a class for footnote links.
|
||||
LinkClass []byte
|
||||
|
||||
// BacklinkClass is a class for footnote backlinks.
|
||||
BacklinkClass []byte
|
||||
|
||||
// BacklinkHTML is an HTML content for footnote backlinks.
|
||||
BacklinkHTML []byte
|
||||
}
|
||||
|
||||
// FootnoteOption interface is a functional option interface for the extension.
|
||||
type FootnoteOption interface {
|
||||
renderer.Option
|
||||
// SetFootnoteOption sets given option to the extension.
|
||||
SetFootnoteOption(*FootnoteConfig)
|
||||
}
|
||||
|
||||
// NewFootnoteConfig returns a new Config with defaults.
|
||||
func NewFootnoteConfig() FootnoteConfig {
|
||||
return FootnoteConfig{
|
||||
Config: html.NewConfig(),
|
||||
LinkTitle: []byte(""),
|
||||
BacklinkTitle: []byte(""),
|
||||
LinkClass: []byte("footnote-ref"),
|
||||
BacklinkClass: []byte("footnote-backref"),
|
||||
BacklinkHTML: []byte("↩︎"),
|
||||
}
|
||||
}
|
||||
|
||||
// SetOption implements renderer.SetOptioner.
|
||||
func (c *FootnoteConfig) SetOption(name renderer.OptionName, value interface{}) {
|
||||
switch name {
|
||||
case optFootnoteIDPrefixFunction:
|
||||
c.IDPrefixFunction = value.(func(gast.Node) []byte)
|
||||
case optFootnoteIDPrefix:
|
||||
c.IDPrefix = value.([]byte)
|
||||
case optFootnoteLinkTitle:
|
||||
c.LinkTitle = value.([]byte)
|
||||
case optFootnoteBacklinkTitle:
|
||||
c.BacklinkTitle = value.([]byte)
|
||||
case optFootnoteLinkClass:
|
||||
c.LinkClass = value.([]byte)
|
||||
case optFootnoteBacklinkClass:
|
||||
c.BacklinkClass = value.([]byte)
|
||||
case optFootnoteBacklinkHTML:
|
||||
c.BacklinkHTML = value.([]byte)
|
||||
default:
|
||||
c.Config.SetOption(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
type withFootnoteHTMLOptions struct {
|
||||
value []html.Option
|
||||
}
|
||||
|
||||
func (o *withFootnoteHTMLOptions) SetConfig(c *renderer.Config) {
|
||||
if o.value != nil {
|
||||
for _, v := range o.value {
|
||||
v.(renderer.Option).SetConfig(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *withFootnoteHTMLOptions) SetFootnoteOption(c *FootnoteConfig) {
|
||||
if o.value != nil {
|
||||
for _, v := range o.value {
|
||||
v.SetHTMLOption(&c.Config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithFootnoteHTMLOptions is functional option that wraps goldmark HTMLRenderer options.
|
||||
func WithFootnoteHTMLOptions(opts ...html.Option) FootnoteOption {
|
||||
return &withFootnoteHTMLOptions{opts}
|
||||
}
|
||||
|
||||
const optFootnoteIDPrefix renderer.OptionName = "FootnoteIDPrefix"
|
||||
|
||||
type withFootnoteIDPrefix struct {
|
||||
value []byte
|
||||
}
|
||||
|
||||
func (o *withFootnoteIDPrefix) SetConfig(c *renderer.Config) {
|
||||
c.Options[optFootnoteIDPrefix] = o.value
|
||||
}
|
||||
|
||||
func (o *withFootnoteIDPrefix) SetFootnoteOption(c *FootnoteConfig) {
|
||||
c.IDPrefix = o.value
|
||||
}
|
||||
|
||||
// WithFootnoteIDPrefix is a functional option that is a prefix for the id attributes generated by footnotes.
|
||||
func WithFootnoteIDPrefix[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteIDPrefix{[]byte(a)}
|
||||
}
|
||||
|
||||
const optFootnoteIDPrefixFunction renderer.OptionName = "FootnoteIDPrefixFunction"
|
||||
|
||||
type withFootnoteIDPrefixFunction struct {
|
||||
value func(gast.Node) []byte
|
||||
}
|
||||
|
||||
func (o *withFootnoteIDPrefixFunction) SetConfig(c *renderer.Config) {
|
||||
c.Options[optFootnoteIDPrefixFunction] = o.value
|
||||
}
|
||||
|
||||
func (o *withFootnoteIDPrefixFunction) SetFootnoteOption(c *FootnoteConfig) {
|
||||
c.IDPrefixFunction = o.value
|
||||
}
|
||||
|
||||
// WithFootnoteIDPrefixFunction is a functional option that is a prefix for the id attributes generated by footnotes.
|
||||
func WithFootnoteIDPrefixFunction(a func(gast.Node) []byte) FootnoteOption {
|
||||
return &withFootnoteIDPrefixFunction{a}
|
||||
}
|
||||
|
||||
const optFootnoteLinkTitle renderer.OptionName = "FootnoteLinkTitle"
|
||||
|
||||
type withFootnoteLinkTitle struct {
|
||||
value []byte
|
||||
}
|
||||
|
||||
func (o *withFootnoteLinkTitle) SetConfig(c *renderer.Config) {
|
||||
c.Options[optFootnoteLinkTitle] = o.value
|
||||
}
|
||||
|
||||
func (o *withFootnoteLinkTitle) SetFootnoteOption(c *FootnoteConfig) {
|
||||
c.LinkTitle = o.value
|
||||
}
|
||||
|
||||
// WithFootnoteLinkTitle is a functional option that is an optional title attribute for footnote links.
|
||||
func WithFootnoteLinkTitle[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteLinkTitle{[]byte(a)}
|
||||
}
|
||||
|
||||
const optFootnoteBacklinkTitle renderer.OptionName = "FootnoteBacklinkTitle"
|
||||
|
||||
type withFootnoteBacklinkTitle struct {
|
||||
value []byte
|
||||
}
|
||||
|
||||
func (o *withFootnoteBacklinkTitle) SetConfig(c *renderer.Config) {
|
||||
c.Options[optFootnoteBacklinkTitle] = o.value
|
||||
}
|
||||
|
||||
func (o *withFootnoteBacklinkTitle) SetFootnoteOption(c *FootnoteConfig) {
|
||||
c.BacklinkTitle = o.value
|
||||
}
|
||||
|
||||
// WithFootnoteBacklinkTitle is a functional option that is an optional title attribute for footnote backlinks.
|
||||
func WithFootnoteBacklinkTitle[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteBacklinkTitle{[]byte(a)}
|
||||
}
|
||||
|
||||
const optFootnoteLinkClass renderer.OptionName = "FootnoteLinkClass"
|
||||
|
||||
type withFootnoteLinkClass struct {
|
||||
value []byte
|
||||
}
|
||||
|
||||
func (o *withFootnoteLinkClass) SetConfig(c *renderer.Config) {
|
||||
c.Options[optFootnoteLinkClass] = o.value
|
||||
}
|
||||
|
||||
func (o *withFootnoteLinkClass) SetFootnoteOption(c *FootnoteConfig) {
|
||||
c.LinkClass = o.value
|
||||
}
|
||||
|
||||
// WithFootnoteLinkClass is a functional option that is a class for footnote links.
|
||||
func WithFootnoteLinkClass[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteLinkClass{[]byte(a)}
|
||||
}
|
||||
|
||||
const optFootnoteBacklinkClass renderer.OptionName = "FootnoteBacklinkClass"
|
||||
|
||||
type withFootnoteBacklinkClass struct {
|
||||
value []byte
|
||||
}
|
||||
|
||||
func (o *withFootnoteBacklinkClass) SetConfig(c *renderer.Config) {
|
||||
c.Options[optFootnoteBacklinkClass] = o.value
|
||||
}
|
||||
|
||||
func (o *withFootnoteBacklinkClass) SetFootnoteOption(c *FootnoteConfig) {
|
||||
c.BacklinkClass = o.value
|
||||
}
|
||||
|
||||
// WithFootnoteBacklinkClass is a functional option that is a class for footnote backlinks.
|
||||
func WithFootnoteBacklinkClass[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteBacklinkClass{[]byte(a)}
|
||||
}
|
||||
|
||||
const optFootnoteBacklinkHTML renderer.OptionName = "FootnoteBacklinkHTML"
|
||||
|
||||
type withFootnoteBacklinkHTML struct {
|
||||
value []byte
|
||||
}
|
||||
|
||||
func (o *withFootnoteBacklinkHTML) SetConfig(c *renderer.Config) {
|
||||
c.Options[optFootnoteBacklinkHTML] = o.value
|
||||
}
|
||||
|
||||
func (o *withFootnoteBacklinkHTML) SetFootnoteOption(c *FootnoteConfig) {
|
||||
c.BacklinkHTML = o.value
|
||||
}
|
||||
|
||||
// WithFootnoteBacklinkHTML is an HTML content for footnote backlinks.
|
||||
func WithFootnoteBacklinkHTML[T []byte | string](a T) FootnoteOption {
|
||||
return &withFootnoteBacklinkHTML{[]byte(a)}
|
||||
}
|
||||
|
||||
// FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that
|
||||
// renders FootnoteLink nodes.
|
||||
type FootnoteHTMLRenderer struct {
|
||||
html.Config
|
||||
FootnoteConfig
|
||||
}
|
||||
|
||||
// NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer.
|
||||
func NewFootnoteHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||
func NewFootnoteHTMLRenderer(opts ...FootnoteOption) renderer.NodeRenderer {
|
||||
r := &FootnoteHTMLRenderer{
|
||||
Config: html.NewConfig(),
|
||||
FootnoteConfig: NewFootnoteConfig(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.SetHTMLOption(&r.Config)
|
||||
opt.SetFootnoteOption(&r.FootnoteConfig)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
|
@ -199,68 +520,158 @@ func NewFootnoteHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
|||
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
|
||||
func (r *FootnoteHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindFootnoteLink, r.renderFootnoteLink)
|
||||
reg.Register(ast.KindFootnoteBacklink, r.renderFootnoteBacklink)
|
||||
reg.Register(ast.KindFootnote, r.renderFootnote)
|
||||
reg.Register(ast.KindFootnoteList, r.renderFootnoteList)
|
||||
}
|
||||
|
||||
func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
func (r *FootnoteHTMLRenderer) renderFootnoteLink(
|
||||
w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
n := node.(*ast.FootnoteLink)
|
||||
is := strconv.Itoa(n.Index)
|
||||
w.WriteString(`<sup id="fnref:`)
|
||||
w.WriteString(is)
|
||||
w.WriteString(`"><a href="#fn:`)
|
||||
w.WriteString(is)
|
||||
w.WriteString(`" class="footnote-ref" role="doc-noteref">`)
|
||||
w.WriteString(is)
|
||||
w.WriteString(`</a></sup>`)
|
||||
_, _ = w.WriteString(`<sup id="`)
|
||||
_, _ = w.Write(r.idPrefix(node))
|
||||
_, _ = w.WriteString(`fnref`)
|
||||
if n.RefIndex > 0 {
|
||||
_, _ = w.WriteString(fmt.Sprintf("%v", n.RefIndex))
|
||||
}
|
||||
_ = w.WriteByte(':')
|
||||
_, _ = w.WriteString(is)
|
||||
_, _ = w.WriteString(`"><a href="#`)
|
||||
_, _ = w.Write(r.idPrefix(node))
|
||||
_, _ = w.WriteString(`fn:`)
|
||||
_, _ = w.WriteString(is)
|
||||
_, _ = w.WriteString(`" class="`)
|
||||
_, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.LinkClass,
|
||||
n.Index, n.RefCount))
|
||||
if len(r.FootnoteConfig.LinkTitle) > 0 {
|
||||
_, _ = w.WriteString(`" title="`)
|
||||
_, _ = w.Write(util.EscapeHTML(applyFootnoteTemplate(r.FootnoteConfig.LinkTitle, n.Index, n.RefCount)))
|
||||
}
|
||||
_, _ = w.WriteString(`" role="doc-noteref">`)
|
||||
|
||||
_, _ = w.WriteString(is)
|
||||
_, _ = w.WriteString(`</a></sup>`)
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
func (r *FootnoteHTMLRenderer) renderFootnoteBacklink(
|
||||
w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
n := node.(*ast.FootnoteBacklink)
|
||||
is := strconv.Itoa(n.Index)
|
||||
_, _ = w.WriteString(` <a href="#`)
|
||||
_, _ = w.Write(r.idPrefix(node))
|
||||
_, _ = w.WriteString(`fnref`)
|
||||
if n.RefIndex > 0 {
|
||||
_, _ = w.WriteString(fmt.Sprintf("%v", n.RefIndex))
|
||||
}
|
||||
_ = w.WriteByte(':')
|
||||
_, _ = w.WriteString(is)
|
||||
_, _ = w.WriteString(`" class="`)
|
||||
_, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.BacklinkClass, n.Index, n.RefCount))
|
||||
if len(r.FootnoteConfig.BacklinkTitle) > 0 {
|
||||
_, _ = w.WriteString(`" title="`)
|
||||
_, _ = w.Write(util.EscapeHTML(applyFootnoteTemplate(r.FootnoteConfig.BacklinkTitle, n.Index, n.RefCount)))
|
||||
}
|
||||
_, _ = w.WriteString(`" role="doc-backlink">`)
|
||||
_, _ = w.Write(applyFootnoteTemplate(r.FootnoteConfig.BacklinkHTML, n.Index, n.RefCount))
|
||||
_, _ = w.WriteString(`</a>`)
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *FootnoteHTMLRenderer) renderFootnote(
|
||||
w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
n := node.(*ast.Footnote)
|
||||
is := strconv.Itoa(n.Index)
|
||||
if entering {
|
||||
w.WriteString(`<li id="fn:`)
|
||||
w.WriteString(is)
|
||||
w.WriteString(`" role="doc-endnote">`)
|
||||
w.WriteString("\n")
|
||||
_, _ = w.WriteString(`<li id="`)
|
||||
_, _ = w.Write(r.idPrefix(node))
|
||||
_, _ = w.WriteString(`fn:`)
|
||||
_, _ = w.WriteString(is)
|
||||
_, _ = w.WriteString(`"`)
|
||||
if node.Attributes() != nil {
|
||||
html.RenderAttributes(w, node, html.ListItemAttributeFilter)
|
||||
}
|
||||
_, _ = w.WriteString(">\n")
|
||||
} else {
|
||||
w.WriteString("</li>\n")
|
||||
_, _ = w.WriteString("</li>\n")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *FootnoteHTMLRenderer) renderFootnoteList(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
tag := "section"
|
||||
if r.Config.XHTML {
|
||||
tag = "div"
|
||||
}
|
||||
func (r *FootnoteHTMLRenderer) renderFootnoteList(
|
||||
w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
w.WriteString("<")
|
||||
w.WriteString(tag)
|
||||
w.WriteString(` class="footnotes" role="doc-endnotes">`)
|
||||
if r.Config.XHTML {
|
||||
w.WriteString("\n<hr />\n")
|
||||
} else {
|
||||
w.WriteString("\n<hr>\n")
|
||||
_, _ = w.WriteString(`<div class="footnotes" role="doc-endnotes"`)
|
||||
if node.Attributes() != nil {
|
||||
html.RenderAttributes(w, node, html.GlobalAttributeFilter)
|
||||
}
|
||||
w.WriteString("<ol>\n")
|
||||
_ = w.WriteByte('>')
|
||||
if r.Config.XHTML {
|
||||
_, _ = w.WriteString("\n<hr />\n")
|
||||
} else {
|
||||
_, _ = w.WriteString("\n<hr>\n")
|
||||
}
|
||||
_, _ = w.WriteString("<ol>\n")
|
||||
} else {
|
||||
w.WriteString("</ol>\n")
|
||||
w.WriteString("<")
|
||||
w.WriteString(tag)
|
||||
w.WriteString(">\n")
|
||||
_, _ = w.WriteString("</ol>\n")
|
||||
_, _ = w.WriteString("</div>\n")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *FootnoteHTMLRenderer) idPrefix(node gast.Node) []byte {
|
||||
if r.FootnoteConfig.IDPrefix != nil {
|
||||
return r.FootnoteConfig.IDPrefix
|
||||
}
|
||||
if r.FootnoteConfig.IDPrefixFunction != nil {
|
||||
return r.FootnoteConfig.IDPrefixFunction(node)
|
||||
}
|
||||
return []byte("")
|
||||
}
|
||||
|
||||
func applyFootnoteTemplate(b []byte, index, refCount int) []byte {
|
||||
fast := true
|
||||
for i, c := range b {
|
||||
if i != 0 {
|
||||
if b[i-1] == '^' && c == '^' {
|
||||
fast = false
|
||||
break
|
||||
}
|
||||
if b[i-1] == '%' && c == '%' {
|
||||
fast = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if fast {
|
||||
return b
|
||||
}
|
||||
is := []byte(strconv.Itoa(index))
|
||||
rs := []byte(strconv.Itoa(refCount))
|
||||
ret := bytes.Replace(b, []byte("^^"), is, -1)
|
||||
return bytes.Replace(ret, []byte("%%"), rs, -1)
|
||||
}
|
||||
|
||||
type footnote struct {
|
||||
options []FootnoteOption
|
||||
}
|
||||
|
||||
// Footnote is an extension that allow you to use PHP Markdown Extra Footnotes.
|
||||
var Footnote = &footnote{}
|
||||
var Footnote = &footnote{
|
||||
options: []FootnoteOption{},
|
||||
}
|
||||
|
||||
// NewFootnote returns a new extension with given options.
|
||||
func NewFootnote(opts ...FootnoteOption) goldmark.Extender {
|
||||
return &footnote{
|
||||
options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *footnote) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(
|
||||
|
|
@ -275,6 +686,6 @@ func (e *footnote) Extend(m goldmark.Markdown) {
|
|||
),
|
||||
)
|
||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||
util.Prioritized(NewFootnoteHTMLRenderer(), 500),
|
||||
util.Prioritized(NewFootnoteHTMLRenderer(e.options...), 500),
|
||||
))
|
||||
}
|
||||
|
|
|
|||
141
extension/footnote_test.go
Normal file
141
extension/footnote_test.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
func TestFootnote(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
Footnote,
|
||||
),
|
||||
)
|
||||
testutil.DoTestCaseFile(markdown, "_test/footnote.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
|
||||
type footnoteID struct {
|
||||
}
|
||||
|
||||
func (a *footnoteID) Transform(node *gast.Document, reader text.Reader, pc parser.Context) {
|
||||
node.Meta()["footnote-prefix"] = "article12-"
|
||||
}
|
||||
|
||||
func TestFootnoteOptions(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewFootnote(
|
||||
WithFootnoteIDPrefix("article12-"),
|
||||
WithFootnoteLinkClass("link-class"),
|
||||
WithFootnoteBacklinkClass("backlink-class"),
|
||||
WithFootnoteLinkTitle("link-title-%%-^^"),
|
||||
WithFootnoteBacklinkTitle("backlink-title"),
|
||||
WithFootnoteBacklinkHTML("^"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Description: "Footnote with options",
|
||||
Markdown: `That's some text with a footnote.[^1]
|
||||
|
||||
Same footnote.[^1]
|
||||
|
||||
Another one.[^2]
|
||||
|
||||
[^1]: And that's the footnote.
|
||||
[^2]: Another footnote.
|
||||
`,
|
||||
Expected: `<p>That's some text with a footnote.<sup id="article12-fnref:1"><a href="#article12-fn:1" class="link-class" title="link-title-2-1" role="doc-noteref">1</a></sup></p>
|
||||
<p>Same footnote.<sup id="article12-fnref1:1"><a href="#article12-fn:1" class="link-class" title="link-title-2-1" role="doc-noteref">1</a></sup></p>
|
||||
<p>Another one.<sup id="article12-fnref:2"><a href="#article12-fn:2" class="link-class" title="link-title-1-2" role="doc-noteref">2</a></sup></p>
|
||||
<div class="footnotes" role="doc-endnotes">
|
||||
<hr>
|
||||
<ol>
|
||||
<li id="article12-fn:1">
|
||||
<p>And that's the footnote. <a href="#article12-fnref:1" class="backlink-class" title="backlink-title" role="doc-backlink">^</a> <a href="#article12-fnref1:1" class="backlink-class" title="backlink-title" role="doc-backlink">^</a></p>
|
||||
</li>
|
||||
<li id="article12-fn:2">
|
||||
<p>Another footnote. <a href="#article12-fnref:2" class="backlink-class" title="backlink-title" role="doc-backlink">^</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
markdown = goldmark.New(
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithASTTransformers(
|
||||
util.Prioritized(&footnoteID{}, 100),
|
||||
),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewFootnote(
|
||||
WithFootnoteIDPrefixFunction(func(n gast.Node) []byte {
|
||||
v, ok := n.OwnerDocument().Meta()["footnote-prefix"]
|
||||
if ok {
|
||||
return util.StringToReadOnlyBytes(v.(string))
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
WithFootnoteLinkClass([]byte("link-class")),
|
||||
WithFootnoteBacklinkClass([]byte("backlink-class")),
|
||||
WithFootnoteLinkTitle([]byte("link-title-%%-^^")),
|
||||
WithFootnoteBacklinkTitle([]byte("backlink-title")),
|
||||
WithFootnoteBacklinkHTML([]byte("^")),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 2,
|
||||
Description: "Footnote with an id prefix function",
|
||||
Markdown: `That's some text with a footnote.[^1]
|
||||
|
||||
Same footnote.[^1]
|
||||
|
||||
Another one.[^2]
|
||||
|
||||
[^1]: And that's the footnote.
|
||||
[^2]: Another footnote.
|
||||
`,
|
||||
Expected: `<p>That's some text with a footnote.<sup id="article12-fnref:1"><a href="#article12-fn:1" class="link-class" title="link-title-2-1" role="doc-noteref">1</a></sup></p>
|
||||
<p>Same footnote.<sup id="article12-fnref1:1"><a href="#article12-fn:1" class="link-class" title="link-title-2-1" role="doc-noteref">1</a></sup></p>
|
||||
<p>Another one.<sup id="article12-fnref:2"><a href="#article12-fn:2" class="link-class" title="link-title-1-2" role="doc-noteref">2</a></sup></p>
|
||||
<div class="footnotes" role="doc-endnotes">
|
||||
<hr>
|
||||
<ol>
|
||||
<li id="article12-fn:1">
|
||||
<p>And that's the footnote. <a href="#article12-fnref:1" class="backlink-class" title="backlink-title" role="doc-backlink">^</a> <a href="#article12-fnref1:1" class="backlink-class" title="backlink-title" role="doc-backlink">^</a></p>
|
||||
</li>
|
||||
<li id="article12-fn:2">
|
||||
<p>Another footnote. <a href="#article12-fnref:2" class="backlink-class" title="backlink-title" role="doc-backlink">^</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</div>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package extension
|
|||
|
||||
import (
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
)
|
||||
|
||||
type gfm struct {
|
||||
|
|
@ -11,20 +10,7 @@ type gfm struct {
|
|||
// GFM is an extension that provides Github Flavored markdown functionalities.
|
||||
var GFM = &gfm{}
|
||||
|
||||
var filterTags = []string{
|
||||
"title",
|
||||
"textarea",
|
||||
"style",
|
||||
"xmp",
|
||||
"iframe",
|
||||
"noembed",
|
||||
"noframes",
|
||||
"script",
|
||||
"plaintext",
|
||||
}
|
||||
|
||||
func (e *gfm) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(parser.WithFilterTags(filterTags...))
|
||||
Linkify.Extend(m)
|
||||
Table.Extend(m)
|
||||
Strikethrough.Extend(m)
|
||||
|
|
|
|||
|
|
@ -2,27 +2,157 @@ package extension
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&//=\(\);]*)`)
|
||||
var wwwURLRegxp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-z]+(?:[/#?][-a-zA-Z0-9@:%_\+.~#!?&/=\(\);,'">\^{}\[\]` + "`" + `]*)?`) //nolint:golint,lll
|
||||
|
||||
var urlRegexp = regexp.MustCompile(`^(?:http|https|ftp):\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=\(\);]*)`)
|
||||
var urlRegexp = regexp.MustCompile(`^(?:http|https|ftp)://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-z]+(?::\d+)?(?:[/#?][-a-zA-Z0-9@:%_+.~#$!?&/=\(\);,'">\^{}\[\]` + "`" + `]*)?`) //nolint:golint,lll
|
||||
|
||||
type linkifyParser struct {
|
||||
// An LinkifyConfig struct is a data structure that holds configuration of the
|
||||
// Linkify extension.
|
||||
type LinkifyConfig struct {
|
||||
AllowedProtocols [][]byte
|
||||
URLRegexp *regexp.Regexp
|
||||
WWWRegexp *regexp.Regexp
|
||||
EmailRegexp *regexp.Regexp
|
||||
}
|
||||
|
||||
var defaultLinkifyParser = &linkifyParser{}
|
||||
const (
|
||||
optLinkifyAllowedProtocols parser.OptionName = "LinkifyAllowedProtocols"
|
||||
optLinkifyURLRegexp parser.OptionName = "LinkifyURLRegexp"
|
||||
optLinkifyWWWRegexp parser.OptionName = "LinkifyWWWRegexp"
|
||||
optLinkifyEmailRegexp parser.OptionName = "LinkifyEmailRegexp"
|
||||
)
|
||||
|
||||
// SetOption implements SetOptioner.
|
||||
func (c *LinkifyConfig) SetOption(name parser.OptionName, value interface{}) {
|
||||
switch name {
|
||||
case optLinkifyAllowedProtocols:
|
||||
c.AllowedProtocols = value.([][]byte)
|
||||
case optLinkifyURLRegexp:
|
||||
c.URLRegexp = value.(*regexp.Regexp)
|
||||
case optLinkifyWWWRegexp:
|
||||
c.WWWRegexp = value.(*regexp.Regexp)
|
||||
case optLinkifyEmailRegexp:
|
||||
c.EmailRegexp = value.(*regexp.Regexp)
|
||||
}
|
||||
}
|
||||
|
||||
// A LinkifyOption interface sets options for the LinkifyOption.
|
||||
type LinkifyOption interface {
|
||||
parser.Option
|
||||
SetLinkifyOption(*LinkifyConfig)
|
||||
}
|
||||
|
||||
type withLinkifyAllowedProtocols struct {
|
||||
value [][]byte
|
||||
}
|
||||
|
||||
func (o *withLinkifyAllowedProtocols) SetParserOption(c *parser.Config) {
|
||||
c.Options[optLinkifyAllowedProtocols] = o.value
|
||||
}
|
||||
|
||||
func (o *withLinkifyAllowedProtocols) SetLinkifyOption(p *LinkifyConfig) {
|
||||
p.AllowedProtocols = o.value
|
||||
}
|
||||
|
||||
// WithLinkifyAllowedProtocols is a functional option that specify allowed
|
||||
// protocols in autolinks. Each protocol must end with ':' like
|
||||
// 'http:' .
|
||||
func WithLinkifyAllowedProtocols[T []byte | string](value []T) LinkifyOption {
|
||||
opt := &withLinkifyAllowedProtocols{}
|
||||
for _, v := range value {
|
||||
opt.value = append(opt.value, []byte(v))
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
type withLinkifyURLRegexp struct {
|
||||
value *regexp.Regexp
|
||||
}
|
||||
|
||||
func (o *withLinkifyURLRegexp) SetParserOption(c *parser.Config) {
|
||||
c.Options[optLinkifyURLRegexp] = o.value
|
||||
}
|
||||
|
||||
func (o *withLinkifyURLRegexp) SetLinkifyOption(p *LinkifyConfig) {
|
||||
p.URLRegexp = o.value
|
||||
}
|
||||
|
||||
// WithLinkifyURLRegexp is a functional option that specify
|
||||
// a pattern of the URL including a protocol.
|
||||
func WithLinkifyURLRegexp(value *regexp.Regexp) LinkifyOption {
|
||||
return &withLinkifyURLRegexp{
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
type withLinkifyWWWRegexp struct {
|
||||
value *regexp.Regexp
|
||||
}
|
||||
|
||||
func (o *withLinkifyWWWRegexp) SetParserOption(c *parser.Config) {
|
||||
c.Options[optLinkifyWWWRegexp] = o.value
|
||||
}
|
||||
|
||||
func (o *withLinkifyWWWRegexp) SetLinkifyOption(p *LinkifyConfig) {
|
||||
p.WWWRegexp = o.value
|
||||
}
|
||||
|
||||
// WithLinkifyWWWRegexp is a functional option that specify
|
||||
// a pattern of the URL without a protocol.
|
||||
// This pattern must start with 'www.' .
|
||||
func WithLinkifyWWWRegexp(value *regexp.Regexp) LinkifyOption {
|
||||
return &withLinkifyWWWRegexp{
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
type withLinkifyEmailRegexp struct {
|
||||
value *regexp.Regexp
|
||||
}
|
||||
|
||||
func (o *withLinkifyEmailRegexp) SetParserOption(c *parser.Config) {
|
||||
c.Options[optLinkifyEmailRegexp] = o.value
|
||||
}
|
||||
|
||||
func (o *withLinkifyEmailRegexp) SetLinkifyOption(p *LinkifyConfig) {
|
||||
p.EmailRegexp = o.value
|
||||
}
|
||||
|
||||
// WithLinkifyEmailRegexp is a functional otpion that specify
|
||||
// a pattern of the email address.
|
||||
func WithLinkifyEmailRegexp(value *regexp.Regexp) LinkifyOption {
|
||||
return &withLinkifyEmailRegexp{
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
type linkifyParser struct {
|
||||
LinkifyConfig
|
||||
}
|
||||
|
||||
// NewLinkifyParser return a new InlineParser can parse
|
||||
// text that seems like a URL.
|
||||
func NewLinkifyParser() parser.InlineParser {
|
||||
return defaultLinkifyParser
|
||||
func NewLinkifyParser(opts ...LinkifyOption) parser.InlineParser {
|
||||
p := &linkifyParser{
|
||||
LinkifyConfig: LinkifyConfig{
|
||||
AllowedProtocols: nil,
|
||||
URLRegexp: urlRegexp,
|
||||
WWWRegexp: wwwURLRegxp,
|
||||
},
|
||||
}
|
||||
for _, o := range opts {
|
||||
o.SetLinkifyOption(&p.LinkifyConfig)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (s *linkifyParser) Trigger() []byte {
|
||||
|
|
@ -30,12 +160,17 @@ func (s *linkifyParser) Trigger() []byte {
|
|||
return []byte{' ', '*', '_', '~', '('}
|
||||
}
|
||||
|
||||
var protoHTTP = []byte("http:")
|
||||
var protoHTTPS = []byte("https:")
|
||||
var protoFTP = []byte("ftp:")
|
||||
var domainWWW = []byte("www.")
|
||||
var (
|
||||
protoHTTP = []byte("http:")
|
||||
protoHTTPS = []byte("https:")
|
||||
protoFTP = []byte("ftp:")
|
||||
domainWWW = []byte("www.")
|
||||
)
|
||||
|
||||
func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
|
||||
if pc.IsInLinkLabel() {
|
||||
return nil
|
||||
}
|
||||
line, segment := block.PeekLine()
|
||||
consumes := 0
|
||||
start := segment.Start
|
||||
|
|
@ -48,15 +183,28 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
|
|||
}
|
||||
|
||||
var m []int
|
||||
typ := ast.AutoLinkType(ast.AutoLinkEmail)
|
||||
typ = ast.AutoLinkURL
|
||||
if bytes.HasPrefix(line, protoHTTP) || bytes.HasPrefix(line, protoHTTPS) || bytes.HasPrefix(line, protoFTP) {
|
||||
m = urlRegexp.FindSubmatchIndex(line)
|
||||
var protocol []byte
|
||||
var typ ast.AutoLinkType = ast.AutoLinkURL
|
||||
if s.LinkifyConfig.AllowedProtocols == nil {
|
||||
if bytes.HasPrefix(line, protoHTTP) || bytes.HasPrefix(line, protoHTTPS) || bytes.HasPrefix(line, protoFTP) {
|
||||
m = s.LinkifyConfig.URLRegexp.FindSubmatchIndex(line)
|
||||
}
|
||||
} else {
|
||||
for _, prefix := range s.LinkifyConfig.AllowedProtocols {
|
||||
if bytes.HasPrefix(line, prefix) {
|
||||
m = s.LinkifyConfig.URLRegexp.FindSubmatchIndex(line)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if m == nil && bytes.HasPrefix(line, domainWWW) {
|
||||
m = wwwURLRegxp.FindSubmatchIndex(line)
|
||||
m = s.LinkifyConfig.WWWRegexp.FindSubmatchIndex(line)
|
||||
protocol = []byte("http")
|
||||
}
|
||||
if m != nil {
|
||||
if m != nil && m[0] != 0 {
|
||||
m = nil
|
||||
}
|
||||
if m != nil && m[0] == 0 {
|
||||
lastChar := line[m[1]-1]
|
||||
if lastChar == '.' {
|
||||
m[1]--
|
||||
|
|
@ -70,7 +218,7 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
|
|||
}
|
||||
}
|
||||
if closing > 0 {
|
||||
m[1]--
|
||||
m[1] -= closing
|
||||
}
|
||||
} else if lastChar == ';' {
|
||||
i := m[1] - 2
|
||||
|
|
@ -88,8 +236,19 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
|
|||
}
|
||||
}
|
||||
if m == nil {
|
||||
if len(line) > 0 && util.IsPunct(line[0]) {
|
||||
return nil
|
||||
}
|
||||
typ = ast.AutoLinkEmail
|
||||
stop := util.FindEmailIndex(line)
|
||||
stop := -1
|
||||
if s.LinkifyConfig.EmailRegexp == nil {
|
||||
stop = util.FindEmailIndex(line)
|
||||
} else {
|
||||
m := s.LinkifyConfig.EmailRegexp.FindSubmatchIndex(line)
|
||||
if m != nil && m[0] == 0 {
|
||||
stop = m[1]
|
||||
}
|
||||
}
|
||||
if stop < 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -116,10 +275,23 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont
|
|||
s := segment.WithStop(segment.Start + 1)
|
||||
ast.MergeOrAppendTextSegment(parent, s)
|
||||
}
|
||||
consumes += m[1]
|
||||
i := m[1] - 1
|
||||
for ; i > 0; i-- {
|
||||
c := line[i]
|
||||
switch c {
|
||||
case '?', '!', '.', ',', ':', '*', '_', '~':
|
||||
default:
|
||||
goto endfor
|
||||
}
|
||||
}
|
||||
endfor:
|
||||
i++
|
||||
consumes += i
|
||||
block.Advance(consumes)
|
||||
n := ast.NewTextSegment(text.NewSegment(start, start+m[1]))
|
||||
return ast.NewAutoLink(typ, n)
|
||||
n := ast.NewTextSegment(text.NewSegment(start, start+i))
|
||||
link := ast.NewAutoLink(typ, n)
|
||||
link.Protocol = protocol
|
||||
return link
|
||||
}
|
||||
|
||||
func (s *linkifyParser) CloseBlock(parent ast.Node, pc parser.Context) {
|
||||
|
|
@ -127,13 +299,24 @@ func (s *linkifyParser) CloseBlock(parent ast.Node, pc parser.Context) {
|
|||
}
|
||||
|
||||
type linkify struct {
|
||||
options []LinkifyOption
|
||||
}
|
||||
|
||||
// Linkify is an extension that allow you to parse text that seems like a URL.
|
||||
var Linkify = &linkify{}
|
||||
|
||||
func (e *linkify) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(parser.WithInlineParsers(
|
||||
util.Prioritized(NewLinkifyParser(), 999),
|
||||
))
|
||||
// NewLinkify creates a new [goldmark.Extender] that
|
||||
// allow you to parse text that seems like a URL.
|
||||
func NewLinkify(opts ...LinkifyOption) goldmark.Extender {
|
||||
return &linkify{
|
||||
options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *linkify) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(
|
||||
parser.WithInlineParsers(
|
||||
util.Prioritized(NewLinkifyParser(e.options...), 999),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
100
extension/linkify_test.go
Normal file
100
extension/linkify_test.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
)
|
||||
|
||||
func TestLinkify(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
Linkify,
|
||||
),
|
||||
)
|
||||
testutil.DoTestCaseFile(markdown, "_test/linkify.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
|
||||
func TestLinkifyWithAllowedProtocols(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewLinkify(
|
||||
WithLinkifyAllowedProtocols([]string{
|
||||
"ssh:",
|
||||
}),
|
||||
WithLinkifyURLRegexp(
|
||||
regexp.MustCompile(`\w+://[^\s]+`),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Markdown: `hoge ssh://user@hoge.com. http://example.com/`,
|
||||
Expected: `<p>hoge <a href="ssh://user@hoge.com">ssh://user@hoge.com</a>. http://example.com/</p>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
||||
func TestLinkifyWithWWWRegexp(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewLinkify(
|
||||
WithLinkifyWWWRegexp(
|
||||
regexp.MustCompile(`www\.example\.com`),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Markdown: `www.google.com www.example.com`,
|
||||
Expected: `<p>www.google.com <a href="http://www.example.com">www.example.com</a></p>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
||||
func TestLinkifyWithEmailRegexp(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewLinkify(
|
||||
WithLinkifyEmailRegexp(
|
||||
regexp.MustCompile(`user@example\.com`),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Markdown: `hoge@example.com user@example.com`,
|
||||
Expected: `<p>hoge@example.com <a href="mailto:user@example.com">user@example.com</a></p>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
2
extension/package.go
Normal file
2
extension/package.go
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Package extension is a collection of builtin extensions.
|
||||
package extension
|
||||
|
|
@ -46,10 +46,11 @@ func (s *strikethroughParser) Trigger() []byte {
|
|||
func (s *strikethroughParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
|
||||
before := block.PrecendingCharacter()
|
||||
line, segment := block.PeekLine()
|
||||
node := parser.ScanDelimiter(line, before, 2, defaultStrikethroughDelimiterProcessor)
|
||||
if node == nil {
|
||||
node := parser.ScanDelimiter(line, before, 1, defaultStrikethroughDelimiterProcessor)
|
||||
if node == nil || node.OriginalLength > 2 || before == '~' {
|
||||
return nil
|
||||
}
|
||||
|
||||
node.Segment = segment.WithStop(segment.Start + node.OriginalLength)
|
||||
block.Advance(node.OriginalLength)
|
||||
pc.PushDelimiter(node)
|
||||
|
|
@ -82,11 +83,21 @@ func (r *StrikethroughHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncR
|
|||
reg.Register(ast.KindStrikethrough, r.renderStrikethrough)
|
||||
}
|
||||
|
||||
func (r *StrikethroughHTMLRenderer) renderStrikethrough(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
// StrikethroughAttributeFilter defines attribute names which dd elements can have.
|
||||
var StrikethroughAttributeFilter = html.GlobalAttributeFilter
|
||||
|
||||
func (r *StrikethroughHTMLRenderer) renderStrikethrough(
|
||||
w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
w.WriteString("<del>")
|
||||
if n.Attributes() != nil {
|
||||
_, _ = w.WriteString("<del")
|
||||
html.RenderAttributes(w, n, StrikethroughAttributeFilter)
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
_, _ = w.WriteString("<del>")
|
||||
}
|
||||
} else {
|
||||
w.WriteString("</del>")
|
||||
_, _ = w.WriteString("</del>")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
|
|
|||
21
extension/strikethrough_test.go
Normal file
21
extension/strikethrough_test.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
)
|
||||
|
||||
func TestStrikethrough(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
Strikethrough,
|
||||
),
|
||||
)
|
||||
testutil.DoTestCaseFile(markdown, "_test/strikethrough.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
|
|
@ -15,7 +15,124 @@ import (
|
|||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
var tableDelimRegexp = regexp.MustCompile(`^[\s\-\|\:]+$`)
|
||||
var escapedPipeCellListKey = parser.NewContextKey()
|
||||
|
||||
type escapedPipeCell struct {
|
||||
Cell *ast.TableCell
|
||||
Pos []int
|
||||
Transformed bool
|
||||
}
|
||||
|
||||
// TableCellAlignMethod indicates how are table cells aligned in HTML format.
|
||||
type TableCellAlignMethod int
|
||||
|
||||
const (
|
||||
// TableCellAlignDefault renders alignments by default method.
|
||||
// With XHTML, alignments are rendered as an align attribute.
|
||||
// With HTML5, alignments are rendered as a style attribute.
|
||||
TableCellAlignDefault TableCellAlignMethod = iota
|
||||
|
||||
// TableCellAlignAttribute renders alignments as an align attribute.
|
||||
TableCellAlignAttribute
|
||||
|
||||
// TableCellAlignStyle renders alignments as a style attribute.
|
||||
TableCellAlignStyle
|
||||
|
||||
// TableCellAlignNone does not care about alignments.
|
||||
// If you using classes or other styles, you can add these attributes
|
||||
// in an ASTTransformer.
|
||||
TableCellAlignNone
|
||||
)
|
||||
|
||||
// TableConfig struct holds options for the extension.
|
||||
type TableConfig struct {
|
||||
html.Config
|
||||
|
||||
// TableCellAlignMethod indicates how are table celss aligned.
|
||||
TableCellAlignMethod TableCellAlignMethod
|
||||
}
|
||||
|
||||
// TableOption interface is a functional option interface for the extension.
|
||||
type TableOption interface {
|
||||
renderer.Option
|
||||
// SetTableOption sets given option to the extension.
|
||||
SetTableOption(*TableConfig)
|
||||
}
|
||||
|
||||
// NewTableConfig returns a new Config with defaults.
|
||||
func NewTableConfig() TableConfig {
|
||||
return TableConfig{
|
||||
Config: html.NewConfig(),
|
||||
TableCellAlignMethod: TableCellAlignDefault,
|
||||
}
|
||||
}
|
||||
|
||||
// SetOption implements renderer.SetOptioner.
|
||||
func (c *TableConfig) SetOption(name renderer.OptionName, value interface{}) {
|
||||
switch name {
|
||||
case optTableCellAlignMethod:
|
||||
c.TableCellAlignMethod = value.(TableCellAlignMethod)
|
||||
default:
|
||||
c.Config.SetOption(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
type withTableHTMLOptions struct {
|
||||
value []html.Option
|
||||
}
|
||||
|
||||
func (o *withTableHTMLOptions) SetConfig(c *renderer.Config) {
|
||||
if o.value != nil {
|
||||
for _, v := range o.value {
|
||||
v.(renderer.Option).SetConfig(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *withTableHTMLOptions) SetTableOption(c *TableConfig) {
|
||||
if o.value != nil {
|
||||
for _, v := range o.value {
|
||||
v.SetHTMLOption(&c.Config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithTableHTMLOptions is functional option that wraps goldmark HTMLRenderer options.
|
||||
func WithTableHTMLOptions(opts ...html.Option) TableOption {
|
||||
return &withTableHTMLOptions{opts}
|
||||
}
|
||||
|
||||
const optTableCellAlignMethod renderer.OptionName = "TableTableCellAlignMethod"
|
||||
|
||||
type withTableCellAlignMethod struct {
|
||||
value TableCellAlignMethod
|
||||
}
|
||||
|
||||
func (o *withTableCellAlignMethod) SetConfig(c *renderer.Config) {
|
||||
c.Options[optTableCellAlignMethod] = o.value
|
||||
}
|
||||
|
||||
func (o *withTableCellAlignMethod) SetTableOption(c *TableConfig) {
|
||||
c.TableCellAlignMethod = o.value
|
||||
}
|
||||
|
||||
// WithTableCellAlignMethod is a functional option that indicates how are table cells aligned in HTML format.
|
||||
func WithTableCellAlignMethod(a TableCellAlignMethod) TableOption {
|
||||
return &withTableCellAlignMethod{a}
|
||||
}
|
||||
|
||||
func isTableDelim(bs []byte) bool {
|
||||
if w, _ := util.IndentWidth(bs, 0); w > 3 {
|
||||
return false
|
||||
}
|
||||
for _, b := range bs {
|
||||
if !(util.IsSpace(b) || b == '-' || b == '|' || b == ':') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var tableDelimLeft = regexp.MustCompile(`^\s*\:\-+\s*$`)
|
||||
var tableDelimRight = regexp.MustCompile(`^\s*\-+\:\s*$`)
|
||||
var tableDelimCenter = regexp.MustCompile(`^\s*\:\-+\:\s*$`)
|
||||
|
|
@ -27,7 +144,7 @@ type tableParagraphTransformer struct {
|
|||
var defaultTableParagraphTransformer = &tableParagraphTransformer{}
|
||||
|
||||
// NewTableParagraphTransformer returns a new ParagraphTransformer
|
||||
// that can transform pargraphs into tables.
|
||||
// that can transform paragraphs into tables.
|
||||
func NewTableParagraphTransformer() parser.ParagraphTransformer {
|
||||
return defaultTableParagraphTransformer
|
||||
}
|
||||
|
|
@ -37,34 +154,41 @@ func (b *tableParagraphTransformer) Transform(node *gast.Paragraph, reader text.
|
|||
if lines.Len() < 2 {
|
||||
return
|
||||
}
|
||||
alignments := b.parseDelimiter(lines.At(1), reader)
|
||||
if alignments == nil {
|
||||
return
|
||||
}
|
||||
header := b.parseRow(lines.At(0), alignments, reader)
|
||||
if header == nil || len(alignments) != header.ChildCount() {
|
||||
return
|
||||
}
|
||||
table := ast.NewTable()
|
||||
table.Alignments = alignments
|
||||
table.AppendChild(table, ast.NewTableHeader(header))
|
||||
if lines.Len() > 2 {
|
||||
for i := 2; i < lines.Len(); i++ {
|
||||
table.AppendChild(table, b.parseRow(lines.At(i), alignments, reader))
|
||||
for i := 1; i < lines.Len(); i++ {
|
||||
alignments := b.parseDelimiter(lines.At(i), reader)
|
||||
if alignments == nil {
|
||||
continue
|
||||
}
|
||||
header := b.parseRow(lines.At(i-1), alignments, true, reader, pc)
|
||||
if header == nil || len(alignments) != header.ChildCount() {
|
||||
return
|
||||
}
|
||||
table := ast.NewTable()
|
||||
table.Alignments = alignments
|
||||
table.AppendChild(table, ast.NewTableHeader(header))
|
||||
for j := i + 1; j < lines.Len(); j++ {
|
||||
table.AppendChild(table, b.parseRow(lines.At(j), alignments, false, reader, pc))
|
||||
}
|
||||
node.Lines().SetSliced(0, i-1)
|
||||
node.Parent().InsertAfter(node.Parent(), node, table)
|
||||
if node.Lines().Len() == 0 {
|
||||
node.Parent().RemoveChild(node.Parent(), node)
|
||||
} else {
|
||||
last := node.Lines().At(i - 2)
|
||||
last.Stop = last.Stop - 1 // trim last newline(\n)
|
||||
node.Lines().Set(i-2, last)
|
||||
}
|
||||
}
|
||||
node.Parent().InsertBefore(node.Parent(), node, table)
|
||||
node.Parent().RemoveChild(node.Parent(), node)
|
||||
return
|
||||
}
|
||||
|
||||
func (b *tableParagraphTransformer) parseRow(segment text.Segment, alignments []ast.Alignment, reader text.Reader) *ast.TableRow {
|
||||
func (b *tableParagraphTransformer) parseRow(segment text.Segment,
|
||||
alignments []ast.Alignment, isHeader bool, reader text.Reader, pc parser.Context) *ast.TableRow {
|
||||
source := reader.Source()
|
||||
segment = segment.TrimLeftSpace(source)
|
||||
segment = segment.TrimRightSpace(source)
|
||||
line := segment.Value(source)
|
||||
pos := 0
|
||||
pos += util.TrimLeftSpaceLength(line)
|
||||
limit := len(line)
|
||||
limit -= util.TrimRightSpaceLength(line)
|
||||
row := ast.NewTableRow(alignments)
|
||||
if len(line) > 0 && line[pos] == '|' {
|
||||
pos++
|
||||
|
|
@ -72,26 +196,60 @@ func (b *tableParagraphTransformer) parseRow(segment text.Segment, alignments []
|
|||
if len(line) > 0 && line[limit-1] == '|' {
|
||||
limit--
|
||||
}
|
||||
for i := 0; pos < limit; i++ {
|
||||
closure := util.FindClosure(line[pos:], byte(0), '|', true, false)
|
||||
if closure < 0 {
|
||||
closure = len(line[pos:])
|
||||
i := 0
|
||||
for ; pos < limit; i++ {
|
||||
alignment := ast.AlignNone
|
||||
if i >= len(alignments) {
|
||||
if !isHeader {
|
||||
return row
|
||||
}
|
||||
} else {
|
||||
alignment = alignments[i]
|
||||
}
|
||||
|
||||
var escapedCell *escapedPipeCell
|
||||
node := ast.NewTableCell()
|
||||
segment := text.NewSegment(segment.Start+pos, segment.Start+pos+closure)
|
||||
segment = segment.TrimLeftSpace(source)
|
||||
segment = segment.TrimRightSpace(source)
|
||||
node.Lines().Append(segment)
|
||||
node.Alignment = alignments[i]
|
||||
node.Alignment = alignment
|
||||
hasBacktick := false
|
||||
closure := pos
|
||||
for ; closure < limit; closure++ {
|
||||
if line[closure] == '`' {
|
||||
hasBacktick = true
|
||||
}
|
||||
if line[closure] == '|' {
|
||||
if closure == 0 || line[closure-1] != '\\' {
|
||||
break
|
||||
} else if hasBacktick {
|
||||
if escapedCell == nil {
|
||||
escapedCell = &escapedPipeCell{node, []int{}, false}
|
||||
escapedList := pc.ComputeIfAbsent(escapedPipeCellListKey,
|
||||
func() interface{} {
|
||||
return []*escapedPipeCell{}
|
||||
}).([]*escapedPipeCell)
|
||||
escapedList = append(escapedList, escapedCell)
|
||||
pc.Set(escapedPipeCellListKey, escapedList)
|
||||
}
|
||||
escapedCell.Pos = append(escapedCell.Pos, segment.Start+closure-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
seg := text.NewSegment(segment.Start+pos, segment.Start+closure)
|
||||
seg = seg.TrimLeftSpace(source)
|
||||
seg = seg.TrimRightSpace(source)
|
||||
node.Lines().Append(seg)
|
||||
row.AppendChild(row, node)
|
||||
pos += closure + 1
|
||||
pos = closure + 1
|
||||
}
|
||||
for ; i < len(alignments); i++ {
|
||||
row.AppendChild(row, ast.NewTableCell())
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func (b *tableParagraphTransformer) parseDelimiter(segment text.Segment, reader text.Reader) []ast.Alignment {
|
||||
|
||||
line := segment.Value(reader.Source())
|
||||
if !tableDelimRegexp.Match(line) {
|
||||
if !isTableDelim(line) {
|
||||
return nil
|
||||
}
|
||||
cols := bytes.Split(line, []byte{'|'})
|
||||
|
|
@ -105,24 +263,12 @@ func (b *tableParagraphTransformer) parseDelimiter(segment text.Segment, reader
|
|||
var alignments []ast.Alignment
|
||||
for _, col := range cols {
|
||||
if tableDelimLeft.Match(col) {
|
||||
if alignments == nil {
|
||||
alignments = []ast.Alignment{}
|
||||
}
|
||||
alignments = append(alignments, ast.AlignLeft)
|
||||
} else if tableDelimRight.Match(col) {
|
||||
if alignments == nil {
|
||||
alignments = []ast.Alignment{}
|
||||
}
|
||||
alignments = append(alignments, ast.AlignRight)
|
||||
} else if tableDelimCenter.Match(col) {
|
||||
if alignments == nil {
|
||||
alignments = []ast.Alignment{}
|
||||
}
|
||||
alignments = append(alignments, ast.AlignCenter)
|
||||
} else if tableDelimNone.Match(col) {
|
||||
if alignments == nil {
|
||||
alignments = []ast.Alignment{}
|
||||
}
|
||||
alignments = append(alignments, ast.AlignNone)
|
||||
} else {
|
||||
return nil
|
||||
|
|
@ -131,19 +277,74 @@ func (b *tableParagraphTransformer) parseDelimiter(segment text.Segment, reader
|
|||
return alignments
|
||||
}
|
||||
|
||||
type tableASTTransformer struct {
|
||||
}
|
||||
|
||||
var defaultTableASTTransformer = &tableASTTransformer{}
|
||||
|
||||
// NewTableASTTransformer returns a parser.ASTTransformer for tables.
|
||||
func NewTableASTTransformer() parser.ASTTransformer {
|
||||
return defaultTableASTTransformer
|
||||
}
|
||||
|
||||
func (a *tableASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) {
|
||||
lst := pc.Get(escapedPipeCellListKey)
|
||||
if lst == nil {
|
||||
return
|
||||
}
|
||||
pc.Set(escapedPipeCellListKey, nil)
|
||||
for _, v := range lst.([]*escapedPipeCell) {
|
||||
if v.Transformed {
|
||||
continue
|
||||
}
|
||||
_ = gast.Walk(v.Cell, func(n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if !entering || n.Kind() != gast.KindCodeSpan {
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
for c := n.FirstChild(); c != nil; {
|
||||
next := c.NextSibling()
|
||||
if c.Kind() != gast.KindText {
|
||||
c = next
|
||||
continue
|
||||
}
|
||||
parent := c.Parent()
|
||||
ts := &c.(*gast.Text).Segment
|
||||
n := c
|
||||
for _, v := range lst.([]*escapedPipeCell) {
|
||||
for _, pos := range v.Pos {
|
||||
if ts.Start <= pos && pos < ts.Stop {
|
||||
segment := n.(*gast.Text).Segment
|
||||
n1 := gast.NewRawTextSegment(segment.WithStop(pos))
|
||||
n2 := gast.NewRawTextSegment(segment.WithStart(pos + 1))
|
||||
parent.InsertAfter(parent, n, n1)
|
||||
parent.InsertAfter(parent, n1, n2)
|
||||
parent.RemoveChild(parent, n)
|
||||
n = n2
|
||||
v.Transformed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
c = next
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TableHTMLRenderer is a renderer.NodeRenderer implementation that
|
||||
// renders Table nodes.
|
||||
type TableHTMLRenderer struct {
|
||||
html.Config
|
||||
TableConfig
|
||||
}
|
||||
|
||||
// NewTableHTMLRenderer returns a new TableHTMLRenderer.
|
||||
func NewTableHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||
func NewTableHTMLRenderer(opts ...TableOption) renderer.NodeRenderer {
|
||||
r := &TableHTMLRenderer{
|
||||
Config: html.NewConfig(),
|
||||
TableConfig: NewTableConfig(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.SetHTMLOption(&r.Config)
|
||||
opt.SetTableOption(&r.TableConfig)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
|
@ -156,73 +357,208 @@ func (r *TableHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegistere
|
|||
reg.Register(ast.KindTableCell, r.renderTableCell)
|
||||
}
|
||||
|
||||
func (r *TableHTMLRenderer) renderTable(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
// TableAttributeFilter defines attribute names which table elements can have.
|
||||
var TableAttributeFilter = html.GlobalAttributeFilter.Extend(
|
||||
[]byte("align"), // [Deprecated]
|
||||
[]byte("bgcolor"), // [Deprecated]
|
||||
[]byte("border"), // [Deprecated]
|
||||
[]byte("cellpadding"), // [Deprecated]
|
||||
[]byte("cellspacing"), // [Deprecated]
|
||||
[]byte("frame"), // [Deprecated]
|
||||
[]byte("rules"), // [Deprecated]
|
||||
[]byte("summary"), // [Deprecated]
|
||||
[]byte("width"), // [Deprecated]
|
||||
)
|
||||
|
||||
func (r *TableHTMLRenderer) renderTable(
|
||||
w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
w.WriteString("<table>\n")
|
||||
_, _ = w.WriteString("<table")
|
||||
if n.Attributes() != nil {
|
||||
html.RenderAttributes(w, n, TableAttributeFilter)
|
||||
}
|
||||
_, _ = w.WriteString(">\n")
|
||||
} else {
|
||||
w.WriteString("</table>\n")
|
||||
_, _ = w.WriteString("</table>\n")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *TableHTMLRenderer) renderTableHeader(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
// TableHeaderAttributeFilter defines attribute names which <thead> elements can have.
|
||||
var TableHeaderAttributeFilter = html.GlobalAttributeFilter.Extend(
|
||||
[]byte("align"), // [Deprecated since HTML4] [Obsolete since HTML5]
|
||||
[]byte("bgcolor"), // [Not Standardized]
|
||||
[]byte("char"), // [Deprecated since HTML4] [Obsolete since HTML5]
|
||||
[]byte("charoff"), // [Deprecated since HTML4] [Obsolete since HTML5]
|
||||
[]byte("valign"), // [Deprecated since HTML4] [Obsolete since HTML5]
|
||||
)
|
||||
|
||||
func (r *TableHTMLRenderer) renderTableHeader(
|
||||
w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
w.WriteString("<thead>\n")
|
||||
w.WriteString("<tr>\n")
|
||||
_, _ = w.WriteString("<thead")
|
||||
if n.Attributes() != nil {
|
||||
html.RenderAttributes(w, n, TableHeaderAttributeFilter)
|
||||
}
|
||||
_, _ = w.WriteString(">\n")
|
||||
_, _ = w.WriteString("<tr>\n") // Header <tr> has no separate handle
|
||||
} else {
|
||||
w.WriteString("</tr>\n")
|
||||
w.WriteString("</thead>\n")
|
||||
_, _ = w.WriteString("</tr>\n")
|
||||
_, _ = w.WriteString("</thead>\n")
|
||||
if n.NextSibling() != nil {
|
||||
w.WriteString("<tbody>\n")
|
||||
}
|
||||
if n.Parent().LastChild() == n {
|
||||
w.WriteString("</tbody>\n")
|
||||
_, _ = w.WriteString("<tbody>\n")
|
||||
}
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *TableHTMLRenderer) renderTableRow(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
// TableRowAttributeFilter defines attribute names which <tr> elements can have.
|
||||
var TableRowAttributeFilter = html.GlobalAttributeFilter.Extend(
|
||||
[]byte("align"), // [Obsolete since HTML5]
|
||||
[]byte("bgcolor"), // [Obsolete since HTML5]
|
||||
[]byte("char"), // [Obsolete since HTML5]
|
||||
[]byte("charoff"), // [Obsolete since HTML5]
|
||||
[]byte("valign"), // [Obsolete since HTML5]
|
||||
)
|
||||
|
||||
func (r *TableHTMLRenderer) renderTableRow(
|
||||
w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
w.WriteString("<tr>\n")
|
||||
_, _ = w.WriteString("<tr")
|
||||
if n.Attributes() != nil {
|
||||
html.RenderAttributes(w, n, TableRowAttributeFilter)
|
||||
}
|
||||
_, _ = w.WriteString(">\n")
|
||||
} else {
|
||||
w.WriteString("</tr>\n")
|
||||
_, _ = w.WriteString("</tr>\n")
|
||||
if n.Parent().LastChild() == n {
|
||||
w.WriteString("</tbody>\n")
|
||||
_, _ = w.WriteString("</tbody>\n")
|
||||
}
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *TableHTMLRenderer) renderTableCell(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
// TableThCellAttributeFilter defines attribute names which table <th> cells can have.
|
||||
var TableThCellAttributeFilter = html.GlobalAttributeFilter.Extend(
|
||||
[]byte("abbr"), // [OK] Contains a short abbreviated description of the cell's content [NOT OK in <td>]
|
||||
|
||||
[]byte("align"), // [Obsolete since HTML5]
|
||||
[]byte("axis"), // [Obsolete since HTML5]
|
||||
[]byte("bgcolor"), // [Not Standardized]
|
||||
[]byte("char"), // [Obsolete since HTML5]
|
||||
[]byte("charoff"), // [Obsolete since HTML5]
|
||||
|
||||
[]byte("colspan"), // [OK] Number of columns that the cell is to span
|
||||
[]byte("headers"), // [OK] This attribute contains a list of space-separated
|
||||
// strings, each corresponding to the id attribute of the <th> elements that apply to this element
|
||||
|
||||
[]byte("height"), // [Deprecated since HTML4] [Obsolete since HTML5]
|
||||
|
||||
[]byte("rowspan"), // [OK] Number of rows that the cell is to span
|
||||
[]byte("scope"), // [OK] This enumerated attribute defines the cells that
|
||||
// the header (defined in the <th>) element relates to [NOT OK in <td>]
|
||||
|
||||
[]byte("valign"), // [Obsolete since HTML5]
|
||||
[]byte("width"), // [Deprecated since HTML4] [Obsolete since HTML5]
|
||||
)
|
||||
|
||||
// TableTdCellAttributeFilter defines attribute names which table <td> cells can have.
|
||||
var TableTdCellAttributeFilter = html.GlobalAttributeFilter.Extend(
|
||||
[]byte("abbr"), // [Obsolete since HTML5] [OK in <th>]
|
||||
[]byte("align"), // [Obsolete since HTML5]
|
||||
[]byte("axis"), // [Obsolete since HTML5]
|
||||
[]byte("bgcolor"), // [Not Standardized]
|
||||
[]byte("char"), // [Obsolete since HTML5]
|
||||
[]byte("charoff"), // [Obsolete since HTML5]
|
||||
|
||||
[]byte("colspan"), // [OK] Number of columns that the cell is to span
|
||||
[]byte("headers"), // [OK] This attribute contains a list of space-separated
|
||||
// strings, each corresponding to the id attribute of the <th> elements that apply to this element
|
||||
|
||||
[]byte("height"), // [Deprecated since HTML4] [Obsolete since HTML5]
|
||||
|
||||
[]byte("rowspan"), // [OK] Number of rows that the cell is to span
|
||||
|
||||
[]byte("scope"), // [Obsolete since HTML5] [OK in <th>]
|
||||
[]byte("valign"), // [Obsolete since HTML5]
|
||||
[]byte("width"), // [Deprecated since HTML4] [Obsolete since HTML5]
|
||||
)
|
||||
|
||||
func (r *TableHTMLRenderer) renderTableCell(
|
||||
w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
n := node.(*ast.TableCell)
|
||||
tag := "td"
|
||||
if n.Parent().Parent().FirstChild() == n.Parent() {
|
||||
if n.Parent().Kind() == ast.KindTableHeader {
|
||||
tag = "th"
|
||||
}
|
||||
if entering {
|
||||
align := ""
|
||||
_, _ = fmt.Fprintf(w, "<%s", tag)
|
||||
if n.Alignment != ast.AlignNone {
|
||||
align = fmt.Sprintf(` align="%s"`, n.Alignment.String())
|
||||
amethod := r.TableConfig.TableCellAlignMethod
|
||||
if amethod == TableCellAlignDefault {
|
||||
if r.Config.XHTML {
|
||||
amethod = TableCellAlignAttribute
|
||||
} else {
|
||||
amethod = TableCellAlignStyle
|
||||
}
|
||||
}
|
||||
switch amethod {
|
||||
case TableCellAlignAttribute:
|
||||
if _, ok := n.AttributeString("align"); !ok { // Skip align render if overridden
|
||||
_, _ = fmt.Fprintf(w, ` align="%s"`, n.Alignment.String())
|
||||
}
|
||||
case TableCellAlignStyle:
|
||||
v, ok := n.AttributeString("style")
|
||||
var cob util.CopyOnWriteBuffer
|
||||
if ok {
|
||||
cob = util.NewCopyOnWriteBuffer(v.([]byte))
|
||||
cob.AppendByte(';')
|
||||
}
|
||||
style := fmt.Sprintf("text-align:%s", n.Alignment.String())
|
||||
cob.AppendString(style)
|
||||
n.SetAttributeString("style", cob.Bytes())
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(w, "<%s%s>", tag, align)
|
||||
if n.Attributes() != nil {
|
||||
if tag == "td" {
|
||||
html.RenderAttributes(w, n, TableTdCellAttributeFilter) // <td>
|
||||
} else {
|
||||
html.RenderAttributes(w, n, TableThCellAttributeFilter) // <th>
|
||||
}
|
||||
}
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
fmt.Fprintf(w, "</%s>\n", tag)
|
||||
_, _ = fmt.Fprintf(w, "</%s>\n", tag)
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
||||
type table struct {
|
||||
options []TableOption
|
||||
}
|
||||
|
||||
// Table is an extension that allow you to use GFM tables .
|
||||
var Table = &table{}
|
||||
var Table = &table{
|
||||
options: []TableOption{},
|
||||
}
|
||||
|
||||
// NewTable returns a new extension with given options.
|
||||
func NewTable(opts ...TableOption) goldmark.Extender {
|
||||
return &table{
|
||||
options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *table) Extend(m goldmark.Markdown) {
|
||||
m.Parser().AddOptions(parser.WithParagraphTransformers(
|
||||
util.Prioritized(NewTableParagraphTransformer(), 200),
|
||||
))
|
||||
m.Parser().AddOptions(
|
||||
parser.WithParagraphTransformers(
|
||||
util.Prioritized(NewTableParagraphTransformer(), 200),
|
||||
),
|
||||
parser.WithASTTransformers(
|
||||
util.Prioritized(defaultTableASTTransformer, 0),
|
||||
),
|
||||
)
|
||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||
util.Prioritized(NewTableHTMLRenderer(), 500),
|
||||
util.Prioritized(NewTableHTMLRenderer(e.options...), 500),
|
||||
))
|
||||
}
|
||||
|
|
|
|||
394
extension/table_test.go
Normal file
394
extension/table_test.go
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
east "github.com/yuin/goldmark/extension/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
func TestTable(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
html.WithXHTML(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
Table,
|
||||
),
|
||||
)
|
||||
testutil.DoTestCaseFile(markdown, "_test/table.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
|
||||
func TestTableWithAlignDefault(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignDefault),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Description: "Cell with TableCellAlignDefault and XHTML should be rendered as an align attribute",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center">abc</th>
|
||||
<th align="right">defghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">bar</td>
|
||||
<td align="right">baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
markdown = goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignDefault),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 2,
|
||||
Description: "Cell with TableCellAlignDefault and HTML5 should be rendered as a style attribute",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:center">abc</th>
|
||||
<th style="text-align:right">defghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align:center">bar</td>
|
||||
<td style="text-align:right">baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
||||
func TestTableWithAlignAttribute(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignAttribute),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Description: "Cell with TableCellAlignAttribute and XHTML should be rendered as an align attribute",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center">abc</th>
|
||||
<th align="right">defghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">bar</td>
|
||||
<td align="right">baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
markdown = goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignAttribute),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 2,
|
||||
Description: "Cell with TableCellAlignAttribute and HTML5 should be rendered as an align attribute",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center">abc</th>
|
||||
<th align="right">defghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">bar</td>
|
||||
<td align="right">baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
||||
type tableStyleTransformer struct {
|
||||
}
|
||||
|
||||
func (a *tableStyleTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
|
||||
cell := node.FirstChild().FirstChild().FirstChild().(*east.TableCell)
|
||||
cell.SetAttributeString("style", []byte("font-size:1em"))
|
||||
}
|
||||
|
||||
func TestTableWithAlignStyle(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignStyle),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Description: "Cell with TableCellAlignStyle and XHTML should be rendered as a style attribute",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:center">abc</th>
|
||||
<th style="text-align:right">defghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align:center">bar</td>
|
||||
<td style="text-align:right">baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
markdown = goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignStyle),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 2,
|
||||
Description: "Cell with TableCellAlignStyle and HTML5 should be rendered as a style attribute",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:center">abc</th>
|
||||
<th style="text-align:right">defghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align:center">bar</td>
|
||||
<td style="text-align:right">baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
|
||||
markdown = goldmark.New(
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithASTTransformers(
|
||||
util.Prioritized(&tableStyleTransformer{}, 0),
|
||||
),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignStyle),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 3,
|
||||
Description: "Styled cell should not be broken the style by the alignments",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="font-size:1em;text-align:center">abc</th>
|
||||
<th style="text-align:right">defghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="text-align:center">bar</td>
|
||||
<td style="text-align:right">baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
||||
func TestTableWithAlignNone(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(
|
||||
WithTableCellAlignMethod(TableCellAlignNone),
|
||||
),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Description: "Cell with TableCellAlignStyle and XHTML should not be rendered",
|
||||
Markdown: `
|
||||
| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz
|
||||
`,
|
||||
Expected: `<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>abc</th>
|
||||
<th>defghi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>bar</td>
|
||||
<td>baz</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
||||
func TestTableFuzzedPanics(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
NewTable(),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCase(
|
||||
markdown,
|
||||
testutil.MarkdownTestCase{
|
||||
No: 1,
|
||||
Description: "This should not panic",
|
||||
Markdown: "* 0\n-|\n\t0",
|
||||
Expected: `<ul>
|
||||
<li>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>0</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</li>
|
||||
</ul>`,
|
||||
},
|
||||
t,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/extension/ast"
|
||||
|
|
@ -9,7 +11,6 @@ import (
|
|||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var taskListRegexp = regexp.MustCompile(`^\[([\sxX])\]\s*`)
|
||||
|
|
@ -40,6 +41,9 @@ func (s *taskCheckBoxParser) Parse(parent gast.Node, block text.Reader, pc parse
|
|||
return nil
|
||||
}
|
||||
|
||||
if parent.HasChildren() {
|
||||
return nil
|
||||
}
|
||||
if _, ok := parent.Parent().(*gast.ListItem); !ok {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -80,21 +84,22 @@ func (r *TaskCheckBoxHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRe
|
|||
reg.Register(ast.KindTaskCheckBox, r.renderTaskCheckBox)
|
||||
}
|
||||
|
||||
func (r *TaskCheckBoxHTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
func (r *TaskCheckBoxHTMLRenderer) renderTaskCheckBox(
|
||||
w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
n := node.(*ast.TaskCheckBox)
|
||||
|
||||
if n.IsChecked {
|
||||
w.WriteString(`<input checked="" disabled="" type="checkbox"`)
|
||||
_, _ = w.WriteString(`<input checked="" disabled="" type="checkbox"`)
|
||||
} else {
|
||||
w.WriteString(`<input disabled="" type="checkbox"`)
|
||||
_, _ = w.WriteString(`<input disabled="" type="checkbox"`)
|
||||
}
|
||||
if r.XHTML {
|
||||
w.WriteString(" />")
|
||||
_, _ = w.WriteString(" /> ")
|
||||
} else {
|
||||
w.WriteString(">")
|
||||
_, _ = w.WriteString("> ")
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
}
|
||||
|
|
|
|||
21
extension/tasklist_test.go
Normal file
21
extension/tasklist_test.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
)
|
||||
|
||||
func TestTaskList(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
TaskList,
|
||||
),
|
||||
)
|
||||
testutil.DoTestCaseFile(markdown, "_test/tasklist.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
|
|
@ -1,39 +1,61 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
gast "github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/extension/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
var uncloseCounterKey = parser.NewContextKey()
|
||||
|
||||
type unclosedCounter struct {
|
||||
Single int
|
||||
Double int
|
||||
}
|
||||
|
||||
func (u *unclosedCounter) Reset() {
|
||||
u.Single = 0
|
||||
u.Double = 0
|
||||
}
|
||||
|
||||
func getUnclosedCounter(pc parser.Context) *unclosedCounter {
|
||||
v := pc.Get(uncloseCounterKey)
|
||||
if v == nil {
|
||||
v = &unclosedCounter{}
|
||||
pc.Set(uncloseCounterKey, v)
|
||||
}
|
||||
return v.(*unclosedCounter)
|
||||
}
|
||||
|
||||
// TypographicPunctuation is a key of the punctuations that can be replaced with
|
||||
// typographic entities.
|
||||
type TypographicPunctuation int
|
||||
|
||||
const (
|
||||
// LeftSingleQuote is '
|
||||
// LeftSingleQuote is ' .
|
||||
LeftSingleQuote TypographicPunctuation = iota + 1
|
||||
// RightSingleQuote is '
|
||||
// RightSingleQuote is ' .
|
||||
RightSingleQuote
|
||||
// LeftDoubleQuote is "
|
||||
// LeftDoubleQuote is " .
|
||||
LeftDoubleQuote
|
||||
// RightDoubleQuote is "
|
||||
// RightDoubleQuote is " .
|
||||
RightDoubleQuote
|
||||
// EnDash is --
|
||||
// EnDash is -- .
|
||||
EnDash
|
||||
// EmDash is ---
|
||||
// EmDash is --- .
|
||||
EmDash
|
||||
// Ellipsis is ...
|
||||
// Ellipsis is ... .
|
||||
Ellipsis
|
||||
// LeftAngleQuote is <<
|
||||
// LeftAngleQuote is << .
|
||||
LeftAngleQuote
|
||||
// RightAngleQuote is >>
|
||||
// RightAngleQuote is >> .
|
||||
RightAngleQuote
|
||||
// Apostrophe is ' .
|
||||
Apostrophe
|
||||
|
||||
typographicPunctuationMax
|
||||
)
|
||||
|
|
@ -55,6 +77,7 @@ func newDefaultSubstitutions() [][]byte {
|
|||
replacements[Ellipsis] = []byte("…")
|
||||
replacements[LeftAngleQuote] = []byte("«")
|
||||
replacements[RightAngleQuote] = []byte("»")
|
||||
replacements[Apostrophe] = []byte("’")
|
||||
|
||||
return replacements
|
||||
}
|
||||
|
|
@ -92,10 +115,10 @@ func (o *withTypographicSubstitutions) SetTypographerOption(p *TypographerConfig
|
|||
|
||||
// WithTypographicSubstitutions is a functional otpion that specify replacement text
|
||||
// for punctuations.
|
||||
func WithTypographicSubstitutions(values map[TypographicPunctuation][]byte) TypographerOption {
|
||||
func WithTypographicSubstitutions[T []byte | string](values map[TypographicPunctuation]T) TypographerOption {
|
||||
replacements := newDefaultSubstitutions()
|
||||
for k, v := range values {
|
||||
replacements[k] = v
|
||||
replacements[k] = []byte(v)
|
||||
}
|
||||
|
||||
return &withTypographicSubstitutions{replacements}
|
||||
|
|
@ -137,23 +160,24 @@ func NewTypographerParser(opts ...TypographerOption) parser.InlineParser {
|
|||
}
|
||||
|
||||
func (s *typographerParser) Trigger() []byte {
|
||||
return []byte{'\'', '"', '-', '.', '<', '>'}
|
||||
return []byte{'\'', '"', '-', '.', ',', '<', '>', '*', '['}
|
||||
}
|
||||
|
||||
func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
|
||||
before := block.PrecendingCharacter()
|
||||
line, _ := block.PeekLine()
|
||||
c := line[0]
|
||||
if len(line) > 2 {
|
||||
if c == '-' {
|
||||
if s.Substitutions[EmDash] != nil && line[1] == '-' && line[2] == '-' { // ---
|
||||
node := ast.NewTypographicText(s.Substitutions[EmDash])
|
||||
node := gast.NewString(s.Substitutions[EmDash])
|
||||
node.SetCode(true)
|
||||
block.Advance(3)
|
||||
return node
|
||||
}
|
||||
} else if c == '.' {
|
||||
if s.Substitutions[Ellipsis] != nil && line[1] == '.' && line[2] == '.' { // ...
|
||||
node := ast.NewTypographicText(s.Substitutions[Ellipsis])
|
||||
node := gast.NewString(s.Substitutions[Ellipsis])
|
||||
node.SetCode(true)
|
||||
block.Advance(3)
|
||||
return node
|
||||
}
|
||||
|
|
@ -163,51 +187,136 @@ func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser
|
|||
if len(line) > 1 {
|
||||
if c == '<' {
|
||||
if s.Substitutions[LeftAngleQuote] != nil && line[1] == '<' { // <<
|
||||
node := ast.NewTypographicText(s.Substitutions[LeftAngleQuote])
|
||||
node := gast.NewString(s.Substitutions[LeftAngleQuote])
|
||||
node.SetCode(true)
|
||||
block.Advance(2)
|
||||
return node
|
||||
}
|
||||
return nil
|
||||
} else if c == '>' {
|
||||
if s.Substitutions[RightAngleQuote] != nil && line[1] == '>' { // >>
|
||||
node := ast.NewTypographicText(s.Substitutions[RightAngleQuote])
|
||||
node := gast.NewString(s.Substitutions[RightAngleQuote])
|
||||
node.SetCode(true)
|
||||
block.Advance(2)
|
||||
return node
|
||||
}
|
||||
return nil
|
||||
} else if s.Substitutions[EnDash] != nil && c == '-' && line[1] == '-' { // --
|
||||
node := ast.NewTypographicText(s.Substitutions[EnDash])
|
||||
node := gast.NewString(s.Substitutions[EnDash])
|
||||
node.SetCode(true)
|
||||
block.Advance(2)
|
||||
return node
|
||||
}
|
||||
}
|
||||
if c == '\'' || c == '"' {
|
||||
before := block.PrecendingCharacter()
|
||||
d := parser.ScanDelimiter(line, before, 1, defaultTypographerDelimiterProcessor)
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
counter := getUnclosedCounter(pc)
|
||||
if c == '\'' {
|
||||
if s.Substitutions[Apostrophe] != nil {
|
||||
// Handle decade abbrevations such as '90s
|
||||
if d.CanOpen && !d.CanClose && len(line) > 3 &&
|
||||
util.IsNumeric(line[1]) && util.IsNumeric(line[2]) && line[3] == 's' {
|
||||
after := rune(' ')
|
||||
if len(line) > 4 {
|
||||
after = util.ToRune(line, 4)
|
||||
}
|
||||
if len(line) == 3 || util.IsSpaceRune(after) || util.IsPunctRune(after) {
|
||||
node := gast.NewString(s.Substitutions[Apostrophe])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
return node
|
||||
}
|
||||
}
|
||||
// special cases: 'twas, 'em, 'net
|
||||
if len(line) > 1 && (unicode.IsPunct(before) || unicode.IsSpace(before)) &&
|
||||
(line[1] == 't' || line[1] == 'e' || line[1] == 'n' || line[1] == 'l') {
|
||||
node := gast.NewString(s.Substitutions[Apostrophe])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
return node
|
||||
}
|
||||
// Convert normal apostrophes. This is probably more flexible than necessary but
|
||||
// converts any apostrophe in between two alphanumerics.
|
||||
if len(line) > 1 && (unicode.IsDigit(before) || unicode.IsLetter(before)) &&
|
||||
(unicode.IsLetter(util.ToRune(line, 1))) {
|
||||
node := gast.NewString(s.Substitutions[Apostrophe])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
return node
|
||||
}
|
||||
}
|
||||
if s.Substitutions[LeftSingleQuote] != nil && d.CanOpen && !d.CanClose {
|
||||
node := ast.NewTypographicText(s.Substitutions[LeftSingleQuote])
|
||||
nt := LeftSingleQuote
|
||||
// special cases: Alice's, I'm, Don't, You'd
|
||||
if len(line) > 1 && (line[1] == 's' || line[1] == 'm' || line[1] == 't' || line[1] == 'd') &&
|
||||
(len(line) < 3 || util.IsPunct(line[2]) || util.IsSpace(line[2])) {
|
||||
nt = RightSingleQuote
|
||||
}
|
||||
// special cases: I've, I'll, You're
|
||||
if len(line) > 2 && ((line[1] == 'v' && line[2] == 'e') ||
|
||||
(line[1] == 'l' && line[2] == 'l') || (line[1] == 'r' && line[2] == 'e')) &&
|
||||
(len(line) < 4 || util.IsPunct(line[3]) || util.IsSpace(line[3])) {
|
||||
nt = RightSingleQuote
|
||||
}
|
||||
if nt == LeftSingleQuote {
|
||||
counter.Single++
|
||||
}
|
||||
|
||||
node := gast.NewString(s.Substitutions[nt])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
return node
|
||||
}
|
||||
if s.Substitutions[RightSingleQuote] != nil && d.CanClose && !d.CanOpen {
|
||||
node := ast.NewTypographicText(s.Substitutions[RightSingleQuote])
|
||||
block.Advance(1)
|
||||
return node
|
||||
if s.Substitutions[RightSingleQuote] != nil {
|
||||
// plural possesive and abbreviations: Smiths', doin'
|
||||
if len(line) > 1 && unicode.IsSpace(util.ToRune(line, 0)) || unicode.IsPunct(util.ToRune(line, 0)) &&
|
||||
(len(line) > 2 && !unicode.IsDigit(util.ToRune(line, 1))) {
|
||||
node := gast.NewString(s.Substitutions[RightSingleQuote])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
return node
|
||||
}
|
||||
}
|
||||
if s.Substitutions[RightSingleQuote] != nil && counter.Single > 0 {
|
||||
isClose := d.CanClose && !d.CanOpen
|
||||
maybeClose := d.CanClose && d.CanOpen && len(line) > 1 && unicode.IsPunct(util.ToRune(line, 1)) &&
|
||||
(len(line) == 2 || (len(line) > 2 && util.IsPunct(line[2]) || util.IsSpace(line[2])))
|
||||
if isClose || maybeClose {
|
||||
node := gast.NewString(s.Substitutions[RightSingleQuote])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
counter.Single--
|
||||
return node
|
||||
}
|
||||
}
|
||||
}
|
||||
if c == '"' {
|
||||
if s.Substitutions[LeftDoubleQuote] != nil && d.CanOpen && !d.CanClose {
|
||||
node := ast.NewTypographicText(s.Substitutions[LeftDoubleQuote])
|
||||
node := gast.NewString(s.Substitutions[LeftDoubleQuote])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
counter.Double++
|
||||
return node
|
||||
}
|
||||
if s.Substitutions[RightDoubleQuote] != nil && d.CanClose && !d.CanOpen {
|
||||
node := ast.NewTypographicText(s.Substitutions[RightDoubleQuote])
|
||||
block.Advance(1)
|
||||
return node
|
||||
if s.Substitutions[RightDoubleQuote] != nil && counter.Double > 0 {
|
||||
isClose := d.CanClose && !d.CanOpen
|
||||
maybeClose := d.CanClose && d.CanOpen && len(line) > 1 && (unicode.IsPunct(util.ToRune(line, 1))) &&
|
||||
(len(line) == 2 || (len(line) > 2 && util.IsPunct(line[2]) || util.IsSpace(line[2])))
|
||||
if isClose || maybeClose {
|
||||
// special case: "Monitor 21""
|
||||
if len(line) > 1 && line[1] == '"' && unicode.IsDigit(before) {
|
||||
return nil
|
||||
}
|
||||
node := gast.NewString(s.Substitutions[RightDoubleQuote])
|
||||
node.SetCode(true)
|
||||
block.Advance(1)
|
||||
counter.Double--
|
||||
return node
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -215,46 +324,17 @@ func (s *typographerParser) Parse(parent gast.Node, block text.Reader, pc parser
|
|||
}
|
||||
|
||||
func (s *typographerParser) CloseBlock(parent gast.Node, pc parser.Context) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
// TypographerHTMLRenderer is a renderer.NodeRenderer implementation that
|
||||
// renders Typographer nodes.
|
||||
type TypographerHTMLRenderer struct {
|
||||
html.Config
|
||||
}
|
||||
|
||||
// NewTypographerHTMLRenderer returns a new TypographerHTMLRenderer.
|
||||
func NewTypographerHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||
r := &TypographerHTMLRenderer{
|
||||
Config: html.NewConfig(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.SetHTMLOption(&r.Config)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
|
||||
func (r *TypographerHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindTypographicText, r.renderTypographicText)
|
||||
}
|
||||
|
||||
func (r *TypographerHTMLRenderer) renderTypographicText(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
|
||||
if entering {
|
||||
w.Write(n.(*ast.TypographicText).Value)
|
||||
}
|
||||
return gast.WalkContinue, nil
|
||||
getUnclosedCounter(pc).Reset()
|
||||
}
|
||||
|
||||
type typographer struct {
|
||||
options []TypographerOption
|
||||
}
|
||||
|
||||
// Typographer is an extension that repalace punctuations with typographic entities.
|
||||
// Typographer is an extension that replaces punctuations with typographic entities.
|
||||
var Typographer = &typographer{}
|
||||
|
||||
// NewTypographer returns a new Entender that repalace punctuations with typographic entities.
|
||||
// NewTypographer returns a new Extender that replaces punctuations with typographic entities.
|
||||
func NewTypographer(opts ...TypographerOption) goldmark.Extender {
|
||||
return &typographer{
|
||||
options: opts,
|
||||
|
|
@ -265,7 +345,4 @@ func (e *typographer) Extend(m goldmark.Markdown) {
|
|||
m.Parser().AddOptions(parser.WithInlineParsers(
|
||||
util.Prioritized(NewTypographerParser(e.options...), 9999),
|
||||
))
|
||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||
util.Prioritized(NewTypographerHTMLRenderer(), 500),
|
||||
))
|
||||
}
|
||||
|
|
|
|||
21
extension/typographer_test.go
Normal file
21
extension/typographer_test.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package extension
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
)
|
||||
|
||||
func TestTypographer(t *testing.T) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
Typographer,
|
||||
),
|
||||
)
|
||||
testutil.DoTestCaseFile(markdown, "_test/typographer.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
221
extra_test.go
Normal file
221
extra_test.go
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
package goldmark_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
)
|
||||
|
||||
var testTimeoutMultiplier = 1.0
|
||||
|
||||
func init() {
|
||||
m, err := strconv.ParseFloat(os.Getenv("GOLDMARK_TEST_TIMEOUT_MULTIPLIER"), 64)
|
||||
if err == nil {
|
||||
testTimeoutMultiplier = m
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtras(t *testing.T) {
|
||||
markdown := New(WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
))
|
||||
testutil.DoTestCaseFile(markdown, "_test/extra.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
|
||||
func TestEndsWithNonSpaceCharacters(t *testing.T) {
|
||||
markdown := New(WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
))
|
||||
source := []byte("```\na\n```")
|
||||
var b bytes.Buffer
|
||||
err := markdown.Convert(source, &b)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
if b.String() != "<pre><code>a\n</code></pre>\n" {
|
||||
t.Errorf("%s \n---------\n %s", source, b.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWindowsNewLine(t *testing.T) {
|
||||
markdown := New(WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
))
|
||||
source := []byte("a \r\nb\n")
|
||||
var b bytes.Buffer
|
||||
err := markdown.Convert(source, &b)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
if b.String() != "<p>a<br />\nb</p>\n" {
|
||||
t.Errorf("%s\n---------\n%s", source, b.String())
|
||||
}
|
||||
|
||||
source = []byte("a\\\r\nb\r\n")
|
||||
var b2 bytes.Buffer
|
||||
err = markdown.Convert(source, &b2)
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
if b2.String() != "<p>a<br />\nb</p>\n" {
|
||||
t.Errorf("\n%s\n---------\n%s", source, b2.String())
|
||||
}
|
||||
}
|
||||
|
||||
type myIDs struct {
|
||||
}
|
||||
|
||||
func (s *myIDs) Generate(value []byte, kind ast.NodeKind) []byte {
|
||||
return []byte("my-id")
|
||||
}
|
||||
|
||||
func (s *myIDs) Put(value []byte) {
|
||||
}
|
||||
|
||||
func TestAutogeneratedIDs(t *testing.T) {
|
||||
ctx := parser.NewContext(parser.WithIDs(&myIDs{}))
|
||||
markdown := New(WithParserOptions(parser.WithAutoHeadingID()))
|
||||
source := []byte("# Title1\n## Title2")
|
||||
var b bytes.Buffer
|
||||
err := markdown.Convert(source, &b, parser.WithContext(ctx))
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
if b.String() != `<h1 id="my-id">Title1</h1>
|
||||
<h2 id="my-id">Title2</h2>
|
||||
` {
|
||||
t.Errorf("%s\n---------\n%s", source, b.String())
|
||||
}
|
||||
}
|
||||
|
||||
func nowMillis() int64 {
|
||||
// TODO: replace UnixNano to UnixMillis(drops Go1.16 support)
|
||||
return time.Now().UnixNano() / 1000000
|
||||
}
|
||||
|
||||
func TestDeepNestedLabelPerformance(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping performance test in short mode")
|
||||
}
|
||||
markdown := New(WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
))
|
||||
|
||||
started := nowMillis()
|
||||
n := 50000
|
||||
source := []byte(strings.Repeat("[", n) + strings.Repeat("]", n))
|
||||
var b bytes.Buffer
|
||||
_ = markdown.Convert(source, &b)
|
||||
finished := nowMillis()
|
||||
if (finished - started) > int64(5000*testTimeoutMultiplier) {
|
||||
t.Error("Parsing deep nested labels took too long")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManyProcessingInstructionPerformance(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping performance test in short mode")
|
||||
}
|
||||
markdown := New(WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
))
|
||||
|
||||
started := nowMillis()
|
||||
n := 50000
|
||||
source := []byte("a " + strings.Repeat("<?", n))
|
||||
var b bytes.Buffer
|
||||
_ = markdown.Convert(source, &b)
|
||||
finished := nowMillis()
|
||||
if (finished - started) > int64(5000*testTimeoutMultiplier) {
|
||||
t.Error("Parsing processing instructions took too long")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManyCDATAPerformance(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping performance test in short mode")
|
||||
}
|
||||
markdown := New(WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
))
|
||||
|
||||
started := nowMillis()
|
||||
n := 50000
|
||||
source := []byte(strings.Repeat("a <![CDATA[", n))
|
||||
var b bytes.Buffer
|
||||
_ = markdown.Convert(source, &b)
|
||||
finished := nowMillis()
|
||||
if (finished - started) > int64(5000*testTimeoutMultiplier) {
|
||||
t.Error("Parsing processing instructions took too long")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManyDeclPerformance(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping performance test in short mode")
|
||||
}
|
||||
markdown := New(WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
))
|
||||
|
||||
started := nowMillis()
|
||||
n := 50000
|
||||
source := []byte(strings.Repeat("a <!A ", n))
|
||||
var b bytes.Buffer
|
||||
_ = markdown.Convert(source, &b)
|
||||
finished := nowMillis()
|
||||
if (finished - started) > int64(5000*testTimeoutMultiplier) {
|
||||
t.Error("Parsing processing instructions took too long")
|
||||
}
|
||||
}
|
||||
|
||||
func TestManyCommentPerformance(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping performance test in short mode")
|
||||
}
|
||||
markdown := New(WithRendererOptions(
|
||||
html.WithXHTML(),
|
||||
html.WithUnsafe(),
|
||||
))
|
||||
|
||||
started := nowMillis()
|
||||
n := 50000
|
||||
source := []byte(strings.Repeat("a <!-- ", n))
|
||||
var b bytes.Buffer
|
||||
_ = markdown.Convert(source, &b)
|
||||
finished := nowMillis()
|
||||
if (finished - started) > int64(5000*testTimeoutMultiplier) {
|
||||
t.Error("Parsing processing instructions took too long")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDangerousURLStringCase(t *testing.T) {
|
||||
markdown := New()
|
||||
|
||||
source := []byte(`[Basic](javascript:alert('Basic'))
|
||||
[CaseInsensitive](JaVaScRiPt:alert('CaseInsensitive'))
|
||||
`)
|
||||
expected := []byte(`<p><a href="">Basic</a>
|
||||
<a href="">CaseInsensitive</a></p>
|
||||
`)
|
||||
var b bytes.Buffer
|
||||
_ = markdown.Convert(source, &b)
|
||||
if !bytes.Equal(expected, b.Bytes()) {
|
||||
t.Error("Dangerous URL should ignore cases:\n" + string(testutil.DiffPretty(expected, b.Bytes())))
|
||||
}
|
||||
}
|
||||
57
fuzz/fuzz_test.go
Normal file
57
fuzz/fuzz_test.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package fuzz
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
func fuzz(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, orig string) {
|
||||
markdown := goldmark.New(
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAutoHeadingID(),
|
||||
parser.WithAttribute(),
|
||||
),
|
||||
goldmark.WithRendererOptions(
|
||||
html.WithUnsafe(),
|
||||
html.WithXHTML(),
|
||||
),
|
||||
goldmark.WithExtensions(
|
||||
extension.DefinitionList,
|
||||
extension.Footnote,
|
||||
extension.GFM,
|
||||
extension.Typographer,
|
||||
extension.Linkify,
|
||||
extension.Table,
|
||||
extension.TaskList,
|
||||
),
|
||||
)
|
||||
var b bytes.Buffer
|
||||
if err := markdown.Convert(util.StringToReadOnlyBytes(orig), &b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzDefault(f *testing.F) {
|
||||
bs, err := os.ReadFile("../_test/spec.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var testCases []map[string]interface{}
|
||||
if err := json.Unmarshal(bs, &testCases); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, c := range testCases {
|
||||
f.Add(c["markdown"])
|
||||
}
|
||||
fuzz(f)
|
||||
}
|
||||
9
fuzz/oss_fuzz_test.go
Normal file
9
fuzz/oss_fuzz_test.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package fuzz
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func FuzzOss(f *testing.F) {
|
||||
fuzz(f)
|
||||
}
|
||||
2
go.mod
2
go.mod
|
|
@ -1 +1,3 @@
|
|||
module github.com/yuin/goldmark
|
||||
|
||||
go 1.19
|
||||
|
|
|
|||
0
go.sum
Normal file
0
go.sum
Normal file
|
|
@ -2,12 +2,13 @@
|
|||
package goldmark
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
"io"
|
||||
)
|
||||
|
||||
// DefaultParser returns a new Parser that is configured by default values.
|
||||
|
|
|
|||
19
options_test.go
Normal file
19
options_test.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package goldmark_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/testutil"
|
||||
)
|
||||
|
||||
func TestAttributeAndAutoHeadingID(t *testing.T) {
|
||||
markdown := New(
|
||||
WithParserOptions(
|
||||
parser.WithAttribute(),
|
||||
parser.WithAutoHeadingID(),
|
||||
),
|
||||
)
|
||||
testutil.DoTestCaseFile(markdown, "_test/options.txt", t, testutil.ParseCliCaseArg()...)
|
||||
}
|
||||
329
parser/attribute.go
Normal file
329
parser/attribute.go
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
var attrNameID = []byte("id")
|
||||
var attrNameClass = []byte("class")
|
||||
|
||||
// An Attribute is an attribute of the markdown elements.
|
||||
type Attribute struct {
|
||||
Name []byte
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// An Attributes is a collection of attributes.
|
||||
type Attributes []Attribute
|
||||
|
||||
// Find returns a (value, true) if an attribute correspond with given name is found, otherwise (nil, false).
|
||||
func (as Attributes) Find(name []byte) (interface{}, bool) {
|
||||
for _, a := range as {
|
||||
if bytes.Equal(a.Name, name) {
|
||||
return a.Value, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (as Attributes) findUpdate(name []byte, cb func(v interface{}) interface{}) bool {
|
||||
for i, a := range as {
|
||||
if bytes.Equal(a.Name, name) {
|
||||
as[i].Value = cb(a.Value)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ParseAttributes parses attributes into a map.
|
||||
// ParseAttributes returns a parsed attributes and true if could parse
|
||||
// attributes, otherwise nil and false.
|
||||
func ParseAttributes(reader text.Reader) (Attributes, bool) {
|
||||
savedLine, savedPosition := reader.Position()
|
||||
reader.SkipSpaces()
|
||||
if reader.Peek() != '{' {
|
||||
reader.SetPosition(savedLine, savedPosition)
|
||||
return nil, false
|
||||
}
|
||||
reader.Advance(1)
|
||||
attrs := Attributes{}
|
||||
for {
|
||||
if reader.Peek() == '}' {
|
||||
reader.Advance(1)
|
||||
return attrs, true
|
||||
}
|
||||
attr, ok := parseAttribute(reader)
|
||||
if !ok {
|
||||
reader.SetPosition(savedLine, savedPosition)
|
||||
return nil, false
|
||||
}
|
||||
if bytes.Equal(attr.Name, attrNameClass) {
|
||||
if !attrs.findUpdate(attrNameClass, func(v interface{}) interface{} {
|
||||
ret := make([]byte, 0, len(v.([]byte))+1+len(attr.Value.([]byte)))
|
||||
ret = append(ret, v.([]byte)...)
|
||||
return append(append(ret, ' '), attr.Value.([]byte)...)
|
||||
}) {
|
||||
attrs = append(attrs, attr)
|
||||
}
|
||||
} else {
|
||||
attrs = append(attrs, attr)
|
||||
}
|
||||
reader.SkipSpaces()
|
||||
if reader.Peek() == ',' {
|
||||
reader.Advance(1)
|
||||
reader.SkipSpaces()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseAttribute(reader text.Reader) (Attribute, bool) {
|
||||
reader.SkipSpaces()
|
||||
c := reader.Peek()
|
||||
if c == '#' || c == '.' {
|
||||
reader.Advance(1)
|
||||
line, _ := reader.PeekLine()
|
||||
i := 0
|
||||
// HTML5 allows any kind of characters as id, but XHTML restricts characters for id.
|
||||
// CommonMark is basically defined for XHTML(even though it is legacy).
|
||||
// So we restrict id characters.
|
||||
for ; i < len(line) && !util.IsSpace(line[i]) &&
|
||||
(!util.IsPunct(line[i]) || line[i] == '_' ||
|
||||
line[i] == '-' || line[i] == ':' || line[i] == '.'); i++ {
|
||||
}
|
||||
name := attrNameClass
|
||||
if c == '#' {
|
||||
name = attrNameID
|
||||
}
|
||||
reader.Advance(i)
|
||||
return Attribute{Name: name, Value: line[0:i]}, true
|
||||
}
|
||||
line, _ := reader.PeekLine()
|
||||
if len(line) == 0 {
|
||||
return Attribute{}, false
|
||||
}
|
||||
c = line[0]
|
||||
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
c == '_' || c == ':') {
|
||||
return Attribute{}, false
|
||||
}
|
||||
i := 0
|
||||
for ; i < len(line); i++ {
|
||||
c = line[i]
|
||||
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '_' || c == ':' || c == '.' || c == '-') {
|
||||
break
|
||||
}
|
||||
}
|
||||
name := line[:i]
|
||||
reader.Advance(i)
|
||||
reader.SkipSpaces()
|
||||
c = reader.Peek()
|
||||
if c != '=' {
|
||||
return Attribute{}, false
|
||||
}
|
||||
reader.Advance(1)
|
||||
reader.SkipSpaces()
|
||||
value, ok := parseAttributeValue(reader)
|
||||
if !ok {
|
||||
return Attribute{}, false
|
||||
}
|
||||
if bytes.Equal(name, attrNameClass) {
|
||||
if _, ok = value.([]byte); !ok {
|
||||
return Attribute{}, false
|
||||
}
|
||||
}
|
||||
return Attribute{Name: name, Value: value}, true
|
||||
}
|
||||
|
||||
func parseAttributeValue(reader text.Reader) (interface{}, bool) {
|
||||
reader.SkipSpaces()
|
||||
c := reader.Peek()
|
||||
var value interface{}
|
||||
var ok bool
|
||||
switch c {
|
||||
case text.EOF:
|
||||
return Attribute{}, false
|
||||
case '{':
|
||||
value, ok = ParseAttributes(reader)
|
||||
case '[':
|
||||
value, ok = parseAttributeArray(reader)
|
||||
case '"':
|
||||
value, ok = parseAttributeString(reader)
|
||||
default:
|
||||
if c == '-' || c == '+' || util.IsNumeric(c) {
|
||||
value, ok = parseAttributeNumber(reader)
|
||||
} else {
|
||||
value, ok = parseAttributeOthers(reader)
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
func parseAttributeArray(reader text.Reader) ([]interface{}, bool) {
|
||||
reader.Advance(1) // skip [
|
||||
ret := []interface{}{}
|
||||
for i := 0; ; i++ {
|
||||
c := reader.Peek()
|
||||
comma := false
|
||||
if i != 0 && c == ',' {
|
||||
reader.Advance(1)
|
||||
comma = true
|
||||
}
|
||||
if c == ']' {
|
||||
if !comma {
|
||||
reader.Advance(1)
|
||||
return ret, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
reader.SkipSpaces()
|
||||
value, ok := parseAttributeValue(reader)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
ret = append(ret, value)
|
||||
reader.SkipSpaces()
|
||||
}
|
||||
}
|
||||
|
||||
func parseAttributeString(reader text.Reader) ([]byte, bool) {
|
||||
reader.Advance(1) // skip "
|
||||
line, _ := reader.PeekLine()
|
||||
i := 0
|
||||
l := len(line)
|
||||
var buf bytes.Buffer
|
||||
for i < l {
|
||||
c := line[i]
|
||||
if c == '\\' && i != l-1 {
|
||||
n := line[i+1]
|
||||
switch n {
|
||||
case '"', '/', '\\':
|
||||
buf.WriteByte(n)
|
||||
i += 2
|
||||
case 'b':
|
||||
buf.WriteString("\b")
|
||||
i += 2
|
||||
case 'f':
|
||||
buf.WriteString("\f")
|
||||
i += 2
|
||||
case 'n':
|
||||
buf.WriteString("\n")
|
||||
i += 2
|
||||
case 'r':
|
||||
buf.WriteString("\r")
|
||||
i += 2
|
||||
case 't':
|
||||
buf.WriteString("\t")
|
||||
i += 2
|
||||
default:
|
||||
buf.WriteByte('\\')
|
||||
i++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if c == '"' {
|
||||
reader.Advance(i + 1)
|
||||
return buf.Bytes(), true
|
||||
}
|
||||
buf.WriteByte(c)
|
||||
i++
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func scanAttributeDecimal(reader text.Reader, w io.ByteWriter) {
|
||||
for {
|
||||
c := reader.Peek()
|
||||
if util.IsNumeric(c) {
|
||||
_ = w.WriteByte(c)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
reader.Advance(1)
|
||||
}
|
||||
}
|
||||
|
||||
func parseAttributeNumber(reader text.Reader) (float64, bool) {
|
||||
sign := 1
|
||||
c := reader.Peek()
|
||||
if c == '-' {
|
||||
sign = -1
|
||||
reader.Advance(1)
|
||||
} else if c == '+' {
|
||||
reader.Advance(1)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if !util.IsNumeric(reader.Peek()) {
|
||||
return 0, false
|
||||
}
|
||||
scanAttributeDecimal(reader, &buf)
|
||||
if buf.Len() == 0 {
|
||||
return 0, false
|
||||
}
|
||||
c = reader.Peek()
|
||||
if c == '.' {
|
||||
buf.WriteByte(c)
|
||||
reader.Advance(1)
|
||||
scanAttributeDecimal(reader, &buf)
|
||||
}
|
||||
c = reader.Peek()
|
||||
if c == 'e' || c == 'E' {
|
||||
buf.WriteByte(c)
|
||||
reader.Advance(1)
|
||||
c = reader.Peek()
|
||||
if c == '-' || c == '+' {
|
||||
buf.WriteByte(c)
|
||||
reader.Advance(1)
|
||||
}
|
||||
scanAttributeDecimal(reader, &buf)
|
||||
}
|
||||
f, err := strconv.ParseFloat(buf.String(), 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return float64(sign) * f, true
|
||||
}
|
||||
|
||||
var bytesTrue = []byte("true")
|
||||
var bytesFalse = []byte("false")
|
||||
var bytesNull = []byte("null")
|
||||
|
||||
func parseAttributeOthers(reader text.Reader) (interface{}, bool) {
|
||||
line, _ := reader.PeekLine()
|
||||
c := line[0]
|
||||
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
c == '_' || c == ':') {
|
||||
return nil, false
|
||||
}
|
||||
i := 0
|
||||
for ; i < len(line); i++ {
|
||||
c := line[i]
|
||||
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '_' || c == ':' || c == '.' || c == '-') {
|
||||
break
|
||||
}
|
||||
}
|
||||
value := line[:i]
|
||||
reader.Advance(i)
|
||||
if bytes.Equal(value, bytesTrue) {
|
||||
return true, true
|
||||
}
|
||||
if bytes.Equal(value, bytesFalse) {
|
||||
return false, true
|
||||
}
|
||||
if bytes.Equal(value, bytesNull) {
|
||||
return nil, true
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ type HeadingConfig struct {
|
|||
}
|
||||
|
||||
// SetOption implements SetOptioner.
|
||||
func (b *HeadingConfig) SetOption(name OptionName, value interface{}) {
|
||||
func (b *HeadingConfig) SetOption(name OptionName, _ interface{}) {
|
||||
switch name {
|
||||
case optAutoHeadingID:
|
||||
b.AutoHeadingID = true
|
||||
|
|
@ -74,9 +74,16 @@ func NewATXHeadingParser(opts ...HeadingOption) BlockParser {
|
|||
return p
|
||||
}
|
||||
|
||||
func (b *atxHeadingParser) Trigger() []byte {
|
||||
return []byte{'#'}
|
||||
}
|
||||
|
||||
func (b *atxHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||
line, segment := reader.PeekLine()
|
||||
pos := pc.BlockOffset()
|
||||
if pos < 0 {
|
||||
return nil, NoChildren
|
||||
}
|
||||
i := pos
|
||||
for ; i < len(line) && line[i] == '#'; i++ {
|
||||
}
|
||||
|
|
@ -84,62 +91,61 @@ func (b *atxHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context)
|
|||
if i == pos || level > 6 {
|
||||
return nil, NoChildren
|
||||
}
|
||||
if i == len(line) { // alone '#' (without a new line character)
|
||||
return ast.NewHeading(level), NoChildren
|
||||
}
|
||||
l := util.TrimLeftSpaceLength(line[i:])
|
||||
if l == 0 {
|
||||
return nil, NoChildren
|
||||
}
|
||||
start := i + l
|
||||
if start >= len(line) {
|
||||
start = len(line) - 1
|
||||
}
|
||||
origstart := start
|
||||
stop := len(line) - util.TrimRightSpaceLength(line)
|
||||
|
||||
node := ast.NewHeading(level)
|
||||
parsed := false
|
||||
if b.Attribute { // handles special case like ### heading ### {#id}
|
||||
start--
|
||||
closureOpen := -1
|
||||
closureClose := -1
|
||||
for i := start; i < stop; {
|
||||
c := line[i]
|
||||
if util.IsEscapedPunctuation(line, i) {
|
||||
i += 2
|
||||
} else if util.IsSpace(c) && i < stop-1 && line[i+1] == '#' {
|
||||
closureOpen = i + 1
|
||||
j := i + 1
|
||||
for ; j < stop && line[j] == '#'; j++ {
|
||||
closureOpen := -1
|
||||
for j := start; j < stop; {
|
||||
c := line[j]
|
||||
if util.IsEscapedPunctuation(line, j) {
|
||||
j += 2
|
||||
} else if util.IsSpace(c) && j < stop-1 && line[j+1] == '#' {
|
||||
closureOpen = j + 1
|
||||
k := j + 1
|
||||
for ; k < stop && line[k] == '#'; k++ {
|
||||
}
|
||||
closureClose = j
|
||||
closureClose = k
|
||||
break
|
||||
} else {
|
||||
i++
|
||||
j++
|
||||
}
|
||||
}
|
||||
if closureClose > 0 {
|
||||
i := closureClose
|
||||
for ; i < stop && util.IsSpace(line[i]); i++ {
|
||||
}
|
||||
if i < stop-1 || line[i] == '{' {
|
||||
as := i + 1
|
||||
for as < stop {
|
||||
ai := util.FindAttributeIndex(line[as:], true)
|
||||
if ai[0] < 0 {
|
||||
break
|
||||
}
|
||||
node.SetAttribute(line[as+ai[0]:as+ai[1]],
|
||||
line[as+ai[2]:as+ai[3]])
|
||||
as += ai[3]
|
||||
}
|
||||
if line[as] == '}' && (as > stop-2 || util.IsBlank(line[as:])) {
|
||||
parsed = true
|
||||
node.Lines().Append(text.NewSegment(segment.Start+start+1, segment.Start+closureOpen))
|
||||
} else {
|
||||
node.RemoveAttributes()
|
||||
reader.Advance(closureClose)
|
||||
attrs, ok := ParseAttributes(reader)
|
||||
rest, _ := reader.PeekLine()
|
||||
parsed = ok && util.IsBlank(rest)
|
||||
if parsed {
|
||||
for _, attr := range attrs {
|
||||
node.SetAttribute(attr.Name, attr.Value)
|
||||
}
|
||||
node.Lines().Append(text.NewSegment(
|
||||
segment.Start+start+1-segment.Padding,
|
||||
segment.Start+closureOpen-segment.Padding))
|
||||
}
|
||||
}
|
||||
}
|
||||
if !parsed {
|
||||
start = origstart
|
||||
stop := len(line) - util.TrimRightSpaceLength(line)
|
||||
if stop <= start { // empty headings like '##[space]'
|
||||
stop = start + 1
|
||||
stop = start
|
||||
} else {
|
||||
i = stop - 1
|
||||
for ; line[i] == '#' && i >= start; i-- {
|
||||
|
|
@ -152,7 +158,7 @@ func (b *atxHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context)
|
|||
}
|
||||
|
||||
if len(util.TrimRight(line[start:stop], []byte{'#'})) != 0 { // empty heading like '### ###'
|
||||
node.Lines().Append(text.NewSegment(segment.Start+start, segment.Start+stop))
|
||||
node.Lines().Append(text.NewSegment(segment.Start+start-segment.Padding, segment.Start+stop-segment.Padding))
|
||||
}
|
||||
}
|
||||
return node, NoChildren
|
||||
|
|
@ -163,13 +169,19 @@ func (b *atxHeadingParser) Continue(node ast.Node, reader text.Reader, pc Contex
|
|||
}
|
||||
|
||||
func (b *atxHeadingParser) Close(node ast.Node, reader text.Reader, pc Context) {
|
||||
if !b.AutoHeadingID {
|
||||
return
|
||||
}
|
||||
if !b.Attribute {
|
||||
if b.Attribute {
|
||||
_, ok := node.AttributeString("id")
|
||||
if !ok {
|
||||
parseLastLineAttributes(node, reader, pc)
|
||||
}
|
||||
}
|
||||
|
||||
if b.AutoHeadingID {
|
||||
id, ok := node.AttributeString("id")
|
||||
if !ok {
|
||||
generateAutoHeadingID(node.(*ast.Heading), reader, pc)
|
||||
} else {
|
||||
pc.IDs().Put(id.([]byte))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -182,13 +194,55 @@ func (b *atxHeadingParser) CanAcceptIndentedLine() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
var attrAutoHeadingIDPrefix = []byte("heading")
|
||||
var attrNameID = []byte("#")
|
||||
|
||||
func generateAutoHeadingID(node *ast.Heading, reader text.Reader, pc Context) {
|
||||
var line []byte
|
||||
lastIndex := node.Lines().Len() - 1
|
||||
lastLine := node.Lines().At(lastIndex)
|
||||
line := lastLine.Value(reader.Source())
|
||||
headingID := pc.IDs().Generate(line, attrAutoHeadingIDPrefix)
|
||||
if lastIndex > -1 {
|
||||
lastLine := node.Lines().At(lastIndex)
|
||||
line = lastLine.Value(reader.Source())
|
||||
}
|
||||
headingID := pc.IDs().Generate(line, ast.KindHeading)
|
||||
node.SetAttribute(attrNameID, headingID)
|
||||
}
|
||||
|
||||
func parseLastLineAttributes(node ast.Node, reader text.Reader, pc Context) {
|
||||
lastIndex := node.Lines().Len() - 1
|
||||
if lastIndex < 0 { // empty headings
|
||||
return
|
||||
}
|
||||
lastLine := node.Lines().At(lastIndex)
|
||||
line := lastLine.Value(reader.Source())
|
||||
lr := text.NewReader(line)
|
||||
var attrs Attributes
|
||||
var ok bool
|
||||
var start text.Segment
|
||||
var sl int
|
||||
var end text.Segment
|
||||
for {
|
||||
c := lr.Peek()
|
||||
if c == text.EOF {
|
||||
break
|
||||
}
|
||||
if c == '\\' {
|
||||
lr.Advance(1)
|
||||
if lr.Peek() == '{' {
|
||||
lr.Advance(1)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if c == '{' {
|
||||
sl, start = lr.Position()
|
||||
attrs, ok = ParseAttributes(lr)
|
||||
_, end = lr.Position()
|
||||
lr.SetPosition(sl, start)
|
||||
}
|
||||
lr.Advance(1)
|
||||
}
|
||||
if ok && util.IsBlank(line[end.Start:]) {
|
||||
for _, attr := range attrs {
|
||||
node.SetAttribute(attr.Name, attr.Value)
|
||||
}
|
||||
lastLine.Stop = lastLine.Start + start.Start
|
||||
node.Lines().Set(lastIndex, lastLine)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func NewBlockquoteParser() BlockParser {
|
|||
|
||||
func (b *blockquoteParser) process(reader text.Reader) bool {
|
||||
line, _ := reader.PeekLine()
|
||||
w, pos := util.IndentWidth(line, 0)
|
||||
w, pos := util.IndentWidth(line, reader.LineOffset())
|
||||
if w > 3 || pos >= len(line) || line[pos] != '>' {
|
||||
return false
|
||||
}
|
||||
|
|
@ -28,16 +28,21 @@ func (b *blockquoteParser) process(reader text.Reader) bool {
|
|||
reader.Advance(pos)
|
||||
return true
|
||||
}
|
||||
if line[pos] == ' ' || line[pos] == '\t' {
|
||||
pos++
|
||||
}
|
||||
reader.Advance(pos)
|
||||
if line[pos-1] == '\t' {
|
||||
reader.SetPadding(2)
|
||||
if line[pos] == ' ' || line[pos] == '\t' {
|
||||
padding := 0
|
||||
if line[pos] == '\t' {
|
||||
padding = util.TabWidth(reader.LineOffset()) - 1
|
||||
}
|
||||
reader.AdvanceAndSetPadding(1, padding)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *blockquoteParser) Trigger() []byte {
|
||||
return []byte{'>'}
|
||||
}
|
||||
|
||||
func (b *blockquoteParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||
if b.process(reader) {
|
||||
return ast.NewBlockquote(), HasChildren
|
||||
|
|
|
|||
|
|
@ -18,15 +18,24 @@ func NewCodeBlockParser() BlockParser {
|
|||
return defaultCodeBlockParser
|
||||
}
|
||||
|
||||
func (b *codeBlockParser) Trigger() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *codeBlockParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||
line, segment := reader.PeekLine()
|
||||
pos, padding := util.IndentPosition(line, reader.LineOffset(), 4)
|
||||
if pos < 0 {
|
||||
if pos < 0 || util.IsBlank(line) {
|
||||
return nil, NoChildren
|
||||
}
|
||||
node := ast.NewCodeBlock()
|
||||
reader.AdvanceAndSetPadding(pos, padding)
|
||||
_, segment = reader.PeekLine()
|
||||
// if code block line starts with a tab, keep a tab as it is.
|
||||
if segment.Padding != 0 {
|
||||
preserveLeadingTabInCodeBlock(&segment, reader, 0)
|
||||
}
|
||||
segment.ForceNewline = true
|
||||
node.Lines().Append(segment)
|
||||
reader.Advance(segment.Len() - 1)
|
||||
return node, NoChildren
|
||||
|
|
@ -45,6 +54,13 @@ func (b *codeBlockParser) Continue(node ast.Node, reader text.Reader, pc Context
|
|||
}
|
||||
reader.AdvanceAndSetPadding(pos, padding)
|
||||
_, segment = reader.PeekLine()
|
||||
|
||||
// if code block line starts with a tab, keep a tab as it is.
|
||||
if segment.Padding != 0 {
|
||||
preserveLeadingTabInCodeBlock(&segment, reader, 0)
|
||||
}
|
||||
|
||||
segment.ForceNewline = true
|
||||
node.Lines().Append(segment)
|
||||
reader.Advance(segment.Len() - 1)
|
||||
return Continue | NoChildren
|
||||
|
|
@ -55,7 +71,7 @@ func (b *codeBlockParser) Close(node ast.Node, reader text.Reader, pc Context) {
|
|||
lines := node.Lines()
|
||||
length := lines.Len() - 1
|
||||
source := reader.Source()
|
||||
for {
|
||||
for length >= 0 {
|
||||
line := lines.At(length)
|
||||
if util.IsBlank(line.Value(source)) {
|
||||
length--
|
||||
|
|
@ -73,3 +89,14 @@ func (b *codeBlockParser) CanInterruptParagraph() bool {
|
|||
func (b *codeBlockParser) CanAcceptIndentedLine() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func preserveLeadingTabInCodeBlock(segment *text.Segment, reader text.Reader, indent int) {
|
||||
offsetWithPadding := reader.LineOffset() + indent
|
||||
sl, ss := reader.Position()
|
||||
reader.SetPosition(sl, text.NewSegment(ss.Start-1, ss.Stop))
|
||||
if offsetWithPadding == reader.LineOffset() {
|
||||
segment.Padding = 0
|
||||
segment.Start--
|
||||
}
|
||||
reader.SetPosition(sl, ss)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package parser
|
|||
import (
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
type codeSpanParser struct {
|
||||
|
|
@ -42,8 +41,8 @@ func (s *codeSpanParser) Parse(parent ast.Node, block text.Reader, pc Context) a
|
|||
for ; i < len(line) && line[i] == '`'; i++ {
|
||||
}
|
||||
closure := i - oldi
|
||||
if closure == opener && (i+1 >= len(line) || line[i+1] != '`') {
|
||||
segment := segment.WithStop(segment.Start + i - closure)
|
||||
if closure == opener && (i >= len(line) || line[i] != '`') {
|
||||
segment = segment.WithStop(segment.Start + i - closure)
|
||||
if !segment.IsEmpty() {
|
||||
node.AppendChild(node, ast.NewRawTextSegment(segment))
|
||||
}
|
||||
|
|
@ -52,9 +51,7 @@ func (s *codeSpanParser) Parse(parent ast.Node, block text.Reader, pc Context) a
|
|||
}
|
||||
}
|
||||
}
|
||||
if !util.IsBlank(line) {
|
||||
node.AppendChild(node, ast.NewRawTextSegment(segment))
|
||||
}
|
||||
node.AppendChild(node, ast.NewRawTextSegment(segment))
|
||||
block.AdvanceLine()
|
||||
}
|
||||
end:
|
||||
|
|
@ -62,11 +59,11 @@ end:
|
|||
// trim first halfspace and last halfspace
|
||||
segment := node.FirstChild().(*ast.Text).Segment
|
||||
shouldTrimmed := true
|
||||
if !(!segment.IsEmpty() && block.Source()[segment.Start] == ' ') {
|
||||
if !(!segment.IsEmpty() && isSpaceOrNewline(block.Source()[segment.Start])) {
|
||||
shouldTrimmed = false
|
||||
}
|
||||
segment = node.LastChild().(*ast.Text).Segment
|
||||
if !(!segment.IsEmpty() && block.Source()[segment.Stop-1] == ' ') {
|
||||
if !(!segment.IsEmpty() && isSpaceOrNewline(block.Source()[segment.Stop-1])) {
|
||||
shouldTrimmed = false
|
||||
}
|
||||
if shouldTrimmed {
|
||||
|
|
@ -81,3 +78,7 @@ end:
|
|||
}
|
||||
return node
|
||||
}
|
||||
|
||||
func isSpaceOrNewline(c byte) bool {
|
||||
return c == ' ' || c == '\n'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package parser
|
|||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/text"
|
||||
|
|
@ -11,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
// A DelimiterProcessor interface provides a set of functions about
|
||||
// Deliiter nodes.
|
||||
// Delimiter nodes.
|
||||
type DelimiterProcessor interface {
|
||||
// IsDelimiter returns true if given character is a delimiter, otherwise false.
|
||||
IsDelimiter(byte) bool
|
||||
|
|
@ -31,14 +30,14 @@ type Delimiter struct {
|
|||
Segment text.Segment
|
||||
|
||||
// CanOpen is set true if this delimiter can open a span for a new node.
|
||||
// See https://spec.commonmark.org/0.29/#can-open-emphasis for details.
|
||||
// See https://spec.commonmark.org/0.30/#can-open-emphasis for details.
|
||||
CanOpen bool
|
||||
|
||||
// CanClose is set true if this delimiter can close a span for a new node.
|
||||
// See https://spec.commonmark.org/0.29/#can-open-emphasis for details.
|
||||
// See https://spec.commonmark.org/0.30/#can-open-emphasis for details.
|
||||
CanClose bool
|
||||
|
||||
// Length is a remaining length of this delmiter.
|
||||
// Length is a remaining length of this delimiter.
|
||||
Length int
|
||||
|
||||
// OriginalLength is a original length of this delimiter.
|
||||
|
|
@ -67,12 +66,12 @@ func (d *Delimiter) Dump(source []byte, level int) {
|
|||
|
||||
var kindDelimiter = ast.NewNodeKind("Delimiter")
|
||||
|
||||
// Kind implements Node.Kind
|
||||
// Kind implements Node.Kind.
|
||||
func (d *Delimiter) Kind() ast.NodeKind {
|
||||
return kindDelimiter
|
||||
}
|
||||
|
||||
// Text implements Node.Text
|
||||
// Text implements Node.Text.
|
||||
func (d *Delimiter) Text(source []byte) []byte {
|
||||
return d.Segment.Value(source)
|
||||
}
|
||||
|
|
@ -127,15 +126,15 @@ func ScanDelimiter(line []byte, before rune, min int, processor DelimiterProcess
|
|||
after = util.ToRune(line, j)
|
||||
}
|
||||
|
||||
isLeft, isRight, canOpen, canClose := false, false, false, false
|
||||
beforeIsPunctuation := unicode.IsPunct(before)
|
||||
beforeIsWhitespace := unicode.IsSpace(before)
|
||||
afterIsPunctuation := unicode.IsPunct(after)
|
||||
afterIsWhitespace := unicode.IsSpace(after)
|
||||
var canOpen, canClose bool
|
||||
beforeIsPunctuation := util.IsPunctRune(before)
|
||||
beforeIsWhitespace := util.IsSpaceRune(before)
|
||||
afterIsPunctuation := util.IsPunctRune(after)
|
||||
afterIsWhitespace := util.IsSpaceRune(after)
|
||||
|
||||
isLeft = !afterIsWhitespace &&
|
||||
isLeft := !afterIsWhitespace &&
|
||||
(!afterIsPunctuation || beforeIsWhitespace || beforeIsPunctuation)
|
||||
isRight = !beforeIsWhitespace &&
|
||||
isRight := !beforeIsWhitespace &&
|
||||
(!beforeIsPunctuation || afterIsWhitespace || afterIsPunctuation)
|
||||
|
||||
if line[i] == '_' {
|
||||
|
|
@ -156,20 +155,19 @@ func ScanDelimiter(line []byte, before rune, min int, processor DelimiterProcess
|
|||
// If you implement an inline parser that can have other inline nodes as
|
||||
// children, you should call this function when nesting span has closed.
|
||||
func ProcessDelimiters(bottom ast.Node, pc Context) {
|
||||
if pc.LastDelimiter() == nil {
|
||||
lastDelimiter := pc.LastDelimiter()
|
||||
if lastDelimiter == nil {
|
||||
return
|
||||
}
|
||||
var closer *Delimiter
|
||||
if bottom != nil {
|
||||
for c := pc.LastDelimiter().PreviousSibling(); c != nil; {
|
||||
if d, ok := c.(*Delimiter); ok {
|
||||
closer = d
|
||||
if bottom != lastDelimiter {
|
||||
for c := lastDelimiter.PreviousSibling(); c != nil && c != bottom; {
|
||||
if d, ok := c.(*Delimiter); ok {
|
||||
closer = d
|
||||
}
|
||||
c = c.PreviousSibling()
|
||||
}
|
||||
prev := c.PreviousSibling()
|
||||
if prev == bottom {
|
||||
break
|
||||
}
|
||||
c = prev
|
||||
}
|
||||
} else {
|
||||
closer = pc.FirstDelimiter()
|
||||
|
|
@ -187,7 +185,7 @@ func ProcessDelimiters(bottom ast.Node, pc Context) {
|
|||
found := false
|
||||
maybeOpener := false
|
||||
var opener *Delimiter
|
||||
for opener = closer.PreviousDelimiter; opener != nil; opener = opener.PreviousDelimiter {
|
||||
for opener = closer.PreviousDelimiter; opener != nil && opener != bottom; opener = opener.PreviousDelimiter {
|
||||
if opener.CanOpen && opener.Processor.CanOpenCloser(opener, closer) {
|
||||
maybeOpener = true
|
||||
consume = opener.CalcComsumption(closer)
|
||||
|
|
@ -198,10 +196,11 @@ func ProcessDelimiters(bottom ast.Node, pc Context) {
|
|||
}
|
||||
}
|
||||
if !found {
|
||||
next := closer.NextDelimiter
|
||||
if !maybeOpener && !closer.CanOpen {
|
||||
pc.RemoveDelimiter(closer)
|
||||
}
|
||||
closer = closer.NextDelimiter
|
||||
closer = next
|
||||
continue
|
||||
}
|
||||
opener.ConsumeCharacters(consume)
|
||||
|
|
|
|||
|
|
@ -23,14 +23,19 @@ type fenceData struct {
|
|||
char byte
|
||||
indent int
|
||||
length int
|
||||
node ast.Node
|
||||
}
|
||||
|
||||
var fencedCodeBlockInfoKey = NewContextKey()
|
||||
|
||||
func (b *fencedCodeBlockParser) Trigger() []byte {
|
||||
return []byte{'~', '`'}
|
||||
}
|
||||
|
||||
func (b *fencedCodeBlockParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||
line, segment := reader.PeekLine()
|
||||
pos := pc.BlockOffset()
|
||||
if line[pos] != '`' && line[pos] != '~' {
|
||||
if pos < 0 || (line[pos] != '`' && line[pos] != '~') {
|
||||
return nil, NoChildren
|
||||
}
|
||||
findent := pos
|
||||
|
|
@ -47,16 +52,18 @@ func (b *fencedCodeBlockParser) Open(parent ast.Node, reader text.Reader, pc Con
|
|||
rest := line[i:]
|
||||
left := util.TrimLeftSpaceLength(rest)
|
||||
right := util.TrimRightSpaceLength(rest)
|
||||
infoStart, infoStop := segment.Start+i+left, segment.Stop-right
|
||||
value := rest[left : len(rest)-right]
|
||||
if fenceChar == '`' && bytes.IndexByte(value, '`') > -1 {
|
||||
return nil, NoChildren
|
||||
} else if infoStart != infoStop {
|
||||
info = ast.NewTextSegment(text.NewSegment(infoStart, infoStop))
|
||||
if left < len(rest)-right {
|
||||
infoStart, infoStop := segment.Start-segment.Padding+i+left, segment.Stop-right
|
||||
value := rest[left : len(rest)-right]
|
||||
if fenceChar == '`' && bytes.IndexByte(value, '`') > -1 {
|
||||
return nil, NoChildren
|
||||
} else if infoStart != infoStop {
|
||||
info = ast.NewTextSegment(text.NewSegment(infoStart, infoStop))
|
||||
}
|
||||
}
|
||||
}
|
||||
pc.Set(fencedCodeBlockInfoKey, &fenceData{fenceChar, findent, oFenceLength})
|
||||
node := ast.NewFencedCodeBlock(info)
|
||||
pc.Set(fencedCodeBlockInfoKey, &fenceData{fenceChar, findent, oFenceLength, node})
|
||||
return node, NoChildren
|
||||
|
||||
}
|
||||
|
|
@ -64,27 +71,46 @@ func (b *fencedCodeBlockParser) Open(parent ast.Node, reader text.Reader, pc Con
|
|||
func (b *fencedCodeBlockParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||
line, segment := reader.PeekLine()
|
||||
fdata := pc.Get(fencedCodeBlockInfoKey).(*fenceData)
|
||||
w, pos := util.IndentWidth(line, 0)
|
||||
|
||||
w, pos := util.IndentWidth(line, reader.LineOffset())
|
||||
if w < 4 {
|
||||
i := pos
|
||||
for ; i < len(line) && line[i] == fdata.char; i++ {
|
||||
}
|
||||
length := i - pos
|
||||
if length >= fdata.length && util.IsBlank(line[i:]) {
|
||||
reader.Advance(segment.Stop - segment.Start - 1 - segment.Padding)
|
||||
newline := 1
|
||||
if line[len(line)-1] != '\n' {
|
||||
newline = 0
|
||||
}
|
||||
reader.Advance(segment.Stop - segment.Start - newline + segment.Padding)
|
||||
return Close
|
||||
}
|
||||
}
|
||||
|
||||
pos, padding := util.DedentPosition(line, fdata.indent)
|
||||
pos, padding := util.IndentPositionPadding(line, reader.LineOffset(), segment.Padding, fdata.indent)
|
||||
if pos < 0 {
|
||||
pos = util.FirstNonSpacePosition(line)
|
||||
if pos < 0 {
|
||||
pos = 0
|
||||
}
|
||||
padding = 0
|
||||
}
|
||||
seg := text.NewSegmentPadding(segment.Start+pos, segment.Stop, padding)
|
||||
// if code block line starts with a tab, keep a tab as it is.
|
||||
if padding != 0 {
|
||||
preserveLeadingTabInCodeBlock(&seg, reader, fdata.indent)
|
||||
}
|
||||
seg.ForceNewline = true // EOF as newline
|
||||
node.Lines().Append(seg)
|
||||
reader.AdvanceAndSetPadding(segment.Stop-segment.Start-pos-1, padding)
|
||||
return Continue | NoChildren
|
||||
}
|
||||
|
||||
func (b *fencedCodeBlockParser) Close(node ast.Node, reader text.Reader, pc Context) {
|
||||
pc.Set(fencedCodeBlockInfoKey, nil)
|
||||
fdata := pc.Get(fencedCodeBlockInfoKey).(*fenceData)
|
||||
if fdata.node == node {
|
||||
pc.Set(fencedCodeBlockInfoKey, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *fencedCodeBlockParser) CanInterruptParagraph() bool {
|
||||
|
|
|
|||
|
|
@ -2,55 +2,14 @@ package parser
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// An HTMLConfig struct is a data structure that holds configuration of the renderers related to raw htmls.
|
||||
type HTMLConfig struct {
|
||||
FilterTags map[string]bool
|
||||
}
|
||||
|
||||
// SetOption implements SetOptioner.
|
||||
func (b *HTMLConfig) SetOption(name OptionName, value interface{}) {
|
||||
switch name {
|
||||
case optFilterTags:
|
||||
b.FilterTags = value.(map[string]bool)
|
||||
}
|
||||
}
|
||||
|
||||
// A HTMLOption interface sets options for the raw HTML parsers.
|
||||
type HTMLOption interface {
|
||||
Option
|
||||
SetHTMLOption(*HTMLConfig)
|
||||
}
|
||||
|
||||
const optFilterTags OptionName = "FilterTags"
|
||||
|
||||
type withFilterTags struct {
|
||||
value map[string]bool
|
||||
}
|
||||
|
||||
func (o *withFilterTags) SetParserOption(c *Config) {
|
||||
c.Options[optFilterTags] = o.value
|
||||
}
|
||||
|
||||
func (o *withFilterTags) SetHTMLOption(p *HTMLConfig) {
|
||||
p.FilterTags = o.value
|
||||
}
|
||||
|
||||
// WithFilterTags is a functional otpion that specify forbidden tag names.
|
||||
func WithFilterTags(names ...string) HTMLOption {
|
||||
m := map[string]bool{}
|
||||
for _, name := range names {
|
||||
m[name] = true
|
||||
}
|
||||
return &withFilterTags{m}
|
||||
}
|
||||
|
||||
var allowedBlockTags = map[string]bool{
|
||||
"address": true,
|
||||
"article": true,
|
||||
|
|
@ -102,8 +61,8 @@ var allowedBlockTags = map[string]bool{
|
|||
"option": true,
|
||||
"p": true,
|
||||
"param": true,
|
||||
"search": true,
|
||||
"section": true,
|
||||
"source": true,
|
||||
"summary": true,
|
||||
"table": true,
|
||||
"tbody": true,
|
||||
|
|
@ -117,8 +76,8 @@ var allowedBlockTags = map[string]bool{
|
|||
"ul": true,
|
||||
}
|
||||
|
||||
var htmlBlockType1OpenRegexp = regexp.MustCompile(`(?i)^[ ]{0,3}<(script|pre|style)(?:\s.*|>.*|/>.*|)\n?$`)
|
||||
var htmlBlockType1CloseRegexp = regexp.MustCompile(`(?i)^[ ]{0,3}(?:[^ ].*|)</(?:script|pre|style)>.*`)
|
||||
var htmlBlockType1OpenRegexp = regexp.MustCompile(`(?i)^[ ]{0,3}<(script|pre|style|textarea)(?:\s.*|>.*|/>.*|)(?:\r\n|\n)?$`) //nolint:golint,lll
|
||||
var htmlBlockType1CloseRegexp = regexp.MustCompile(`(?i)^.*</(?:script|pre|style|textarea)>.*`)
|
||||
|
||||
var htmlBlockType2OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<!\-\-`)
|
||||
var htmlBlockType2Close = []byte{'-', '-', '>'}
|
||||
|
|
@ -126,41 +85,40 @@ var htmlBlockType2Close = []byte{'-', '-', '>'}
|
|||
var htmlBlockType3OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<\?`)
|
||||
var htmlBlockType3Close = []byte{'?', '>'}
|
||||
|
||||
var htmlBlockType4OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<![A-Z]+.*\n?$`)
|
||||
var htmlBlockType4OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<![A-Z]+.*(?:\r\n|\n)?$`)
|
||||
var htmlBlockType4Close = []byte{'>'}
|
||||
|
||||
var htmlBlockType5OpenRegexp = regexp.MustCompile(`^[ ]{0,3}<\!\[CDATA\[`)
|
||||
var htmlBlockType5Close = []byte{']', ']', '>'}
|
||||
|
||||
var htmlBlockType6Regexp = regexp.MustCompile(`^[ ]{0,3}</?([a-zA-Z0-9]+)(?:\s.*|>.*|/>.*|)\n?$`)
|
||||
var htmlBlockType6Regexp = regexp.MustCompile(`^[ ]{0,3}<(?:/[ ]*)?([a-zA-Z]+[a-zA-Z0-9\-]*)(?:[ ].*|>.*|/>.*|)(?:\r\n|\n)?$`) //nolint:golint,lll
|
||||
|
||||
var htmlBlockType7Regexp = regexp.MustCompile(`^[ ]{0,3}<(/)?([a-zA-Z0-9]+)(` + attributePattern + `*)(:?>|/>)\s*\n?$`)
|
||||
var htmlBlockType7Regexp = regexp.MustCompile(`^[ ]{0,3}<(/[ ]*)?([a-zA-Z]+[a-zA-Z0-9\-]*)(` + attributePattern + `*)[ ]*(?:>|/>)[ ]*(?:\r\n|\n)?$`) //nolint:golint,lll
|
||||
|
||||
type htmlBlockParser struct {
|
||||
HTMLConfig
|
||||
}
|
||||
|
||||
var defaultHTMLBlockParser = &htmlBlockParser{}
|
||||
|
||||
// NewHTMLBlockParser return a new BlockParser that can parse html
|
||||
// blocks.
|
||||
func NewHTMLBlockParser(opts ...HTMLOption) BlockParser {
|
||||
p := &htmlBlockParser{}
|
||||
for _, o := range opts {
|
||||
o.SetHTMLOption(&p.HTMLConfig)
|
||||
}
|
||||
return p
|
||||
func NewHTMLBlockParser() BlockParser {
|
||||
return defaultHTMLBlockParser
|
||||
}
|
||||
|
||||
func (b *htmlBlockParser) Trigger() []byte {
|
||||
return []byte{'<'}
|
||||
}
|
||||
|
||||
func (b *htmlBlockParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||
var node *ast.HTMLBlock
|
||||
line, segment := reader.PeekLine()
|
||||
last := pc.LastOpenedBlock().Node
|
||||
if pos := pc.BlockOffset(); line[pos] != '<' {
|
||||
if pos := pc.BlockOffset(); pos < 0 || line[pos] != '<' {
|
||||
return nil, NoChildren
|
||||
}
|
||||
|
||||
tagName := ""
|
||||
if m := htmlBlockType1OpenRegexp.FindSubmatchIndex(line); m != nil {
|
||||
tagName = string(line[m[2]:m[3]])
|
||||
node = ast.NewHTMLBlock(ast.HTMLBlockType1)
|
||||
} else if htmlBlockType2OpenRegexp.Match(line) {
|
||||
node = ast.NewHTMLBlock(ast.HTMLBlockType2)
|
||||
|
|
@ -173,17 +131,18 @@ func (b *htmlBlockParser) Open(parent ast.Node, reader text.Reader, pc Context)
|
|||
} else if match := htmlBlockType7Regexp.FindSubmatchIndex(line); match != nil {
|
||||
isCloseTag := match[2] > -1 && bytes.Equal(line[match[2]:match[3]], []byte("/"))
|
||||
hasAttr := match[6] != match[7]
|
||||
tagName = strings.ToLower(string(line[match[4]:match[5]]))
|
||||
_, ok := allowedBlockTags[strings.ToLower(string(tagName))]
|
||||
tagName := strings.ToLower(string(line[match[4]:match[5]]))
|
||||
_, ok := allowedBlockTags[tagName]
|
||||
if ok {
|
||||
node = ast.NewHTMLBlock(ast.HTMLBlockType6)
|
||||
} else if tagName != "script" && tagName != "style" && tagName != "pre" && !ast.IsParagraph(last) && !(isCloseTag && hasAttr) { // type 7 can not interrupt paragraph
|
||||
} else if tagName != "script" && tagName != "style" &&
|
||||
tagName != "pre" && !ast.IsParagraph(last) && !(isCloseTag && hasAttr) { // type 7 can not interrupt paragraph
|
||||
node = ast.NewHTMLBlock(ast.HTMLBlockType7)
|
||||
}
|
||||
}
|
||||
if node == nil {
|
||||
if match := htmlBlockType6Regexp.FindSubmatchIndex(line); match != nil {
|
||||
tagName = string(line[match[2]:match[3]])
|
||||
tagName := string(line[match[2]:match[3]])
|
||||
_, ok := allowedBlockTags[strings.ToLower(tagName)]
|
||||
if ok {
|
||||
node = ast.NewHTMLBlock(ast.HTMLBlockType6)
|
||||
|
|
@ -191,12 +150,7 @@ func (b *htmlBlockParser) Open(parent ast.Node, reader text.Reader, pc Context)
|
|||
}
|
||||
}
|
||||
if node != nil {
|
||||
if b.FilterTags != nil {
|
||||
if _, ok := b.FilterTags[tagName]; ok {
|
||||
return nil, NoChildren
|
||||
}
|
||||
}
|
||||
reader.Advance(segment.Len() - 1)
|
||||
reader.Advance(segment.Len() - util.TrimRightSpaceLength(line))
|
||||
node.Lines().Append(segment)
|
||||
return node, NoChildren
|
||||
}
|
||||
|
|
@ -219,7 +173,7 @@ func (b *htmlBlockParser) Continue(node ast.Node, reader text.Reader, pc Context
|
|||
}
|
||||
if htmlBlockType1CloseRegexp.Match(line) {
|
||||
htmlBlock.ClosureLine = segment
|
||||
reader.Advance(segment.Len() - 1)
|
||||
reader.Advance(segment.Len() - util.TrimRightSpaceLength(line))
|
||||
return Close
|
||||
}
|
||||
case ast.HTMLBlockType2:
|
||||
|
|
@ -248,7 +202,7 @@ func (b *htmlBlockParser) Continue(node ast.Node, reader text.Reader, pc Context
|
|||
}
|
||||
if bytes.Contains(line, closurePattern) {
|
||||
htmlBlock.ClosureLine = segment
|
||||
reader.Advance(segment.Len() - 1)
|
||||
reader.Advance(segment.Len())
|
||||
return Close
|
||||
}
|
||||
|
||||
|
|
@ -258,7 +212,7 @@ func (b *htmlBlockParser) Continue(node ast.Node, reader text.Reader, pc Context
|
|||
}
|
||||
}
|
||||
node.Lines().Append(segment)
|
||||
reader.Advance(segment.Len() - 1)
|
||||
reader.Advance(segment.Len() - util.TrimRightSpaceLength(line))
|
||||
return Continue | NoChildren
|
||||
}
|
||||
|
||||
|
|
|
|||
159
parser/link.go
159
parser/link.go
|
|
@ -2,7 +2,6 @@ package parser
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
|
|
@ -49,6 +48,13 @@ func (s *linkLabelState) Kind() ast.NodeKind {
|
|||
return kindLinkLabelState
|
||||
}
|
||||
|
||||
func linkLabelStateLength(v *linkLabelState) int {
|
||||
if v == nil || v.Last == nil || v.First == nil {
|
||||
return 0
|
||||
}
|
||||
return v.Last.Segment.Stop - v.First.Segment.Start
|
||||
}
|
||||
|
||||
func pushLinkLabelState(pc Context, v *linkLabelState) {
|
||||
tlist := pc.Get(linkLabelStateKey)
|
||||
var list *linkLabelState
|
||||
|
|
@ -113,19 +119,20 @@ func (s *linkParser) Trigger() []byte {
|
|||
return []byte{'!', '[', ']'}
|
||||
}
|
||||
|
||||
var linkDestinationRegexp = regexp.MustCompile(`\s*([^\s].+)`)
|
||||
var linkTitleRegexp = regexp.MustCompile(`\s+(\)|["'\(].+)`)
|
||||
var linkBottom = NewContextKey()
|
||||
|
||||
func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.Node {
|
||||
line, segment := block.PeekLine()
|
||||
if line[0] == '!' && len(line) > 1 && line[1] == '[' {
|
||||
block.Advance(1)
|
||||
pc.Set(linkBottom, pc.LastDelimiter())
|
||||
return processLinkLabelOpen(block, segment.Start+1, true, pc)
|
||||
if line[0] == '!' {
|
||||
if len(line) > 1 && line[1] == '[' {
|
||||
block.Advance(1)
|
||||
pushLinkBottom(pc)
|
||||
return processLinkLabelOpen(block, segment.Start+1, true, pc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if line[0] == '[' {
|
||||
pc.Set(linkBottom, pc.LastDelimiter())
|
||||
pushLinkBottom(pc)
|
||||
return processLinkLabelOpen(block, segment.Start, false, pc)
|
||||
}
|
||||
|
||||
|
|
@ -136,17 +143,22 @@ func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.N
|
|||
}
|
||||
last := tlist.(*linkLabelState).Last
|
||||
if last == nil {
|
||||
_ = popLinkBottom(pc)
|
||||
return nil
|
||||
}
|
||||
block.Advance(1)
|
||||
removeLinkLabelState(pc, last)
|
||||
if s.containsLink(last) { // a link in a link text is not allowed
|
||||
// CommonMark spec says:
|
||||
// > A link label can have at most 999 characters inside the square brackets.
|
||||
if linkLabelStateLength(tlist.(*linkLabelState)) > 998 {
|
||||
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
|
||||
_ = popLinkBottom(pc)
|
||||
return nil
|
||||
}
|
||||
labelValue := block.Value(text.NewSegment(last.Segment.Start+1, segment.Start))
|
||||
if util.IsBlank(labelValue) && !last.IsImage {
|
||||
|
||||
if !last.IsImage && s.containsLink(last) { // a link in a link text is not allowed
|
||||
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
|
||||
_ = popLinkBottom(pc)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -160,6 +172,7 @@ func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.N
|
|||
link, hasValue = s.parseReferenceLink(parent, last, block, pc)
|
||||
if link == nil && hasValue {
|
||||
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
|
||||
_ = popLinkBottom(pc)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -169,9 +182,18 @@ func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.N
|
|||
block.SetPosition(l, pos)
|
||||
ssegment := text.NewSegment(last.Segment.Stop, segment.Start)
|
||||
maybeReference := block.Value(ssegment)
|
||||
// CommonMark spec says:
|
||||
// > A link label can have at most 999 characters inside the square brackets.
|
||||
if len(maybeReference) > 999 {
|
||||
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
|
||||
_ = popLinkBottom(pc)
|
||||
return nil
|
||||
}
|
||||
|
||||
ref, ok := pc.Reference(util.ToLinkReference(maybeReference))
|
||||
if !ok {
|
||||
ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
|
||||
_ = popLinkBottom(pc)
|
||||
return nil
|
||||
}
|
||||
link = ast.NewLink()
|
||||
|
|
@ -187,15 +209,17 @@ func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.N
|
|||
return link
|
||||
}
|
||||
|
||||
func (s *linkParser) containsLink(last *linkLabelState) bool {
|
||||
if last.IsImage {
|
||||
func (s *linkParser) containsLink(n ast.Node) bool {
|
||||
if n == nil {
|
||||
return false
|
||||
}
|
||||
var c ast.Node
|
||||
for c = last; c != nil; c = c.NextSibling() {
|
||||
for c := n; c != nil; c = c.NextSibling() {
|
||||
if _, ok := c.(*ast.Link); ok {
|
||||
return true
|
||||
}
|
||||
if s.containsLink(c.FirstChild()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -212,11 +236,7 @@ func processLinkLabelOpen(block text.Reader, pos int, isImage bool, pc Context)
|
|||
}
|
||||
|
||||
func (s *linkParser) processLinkLabel(parent ast.Node, link *ast.Link, last *linkLabelState, pc Context) {
|
||||
var bottom ast.Node
|
||||
if v := pc.Get(linkBottom); v != nil {
|
||||
bottom = v.(ast.Node)
|
||||
}
|
||||
pc.Set(linkBottom, nil)
|
||||
bottom := popLinkBottom(pc)
|
||||
ProcessDelimiters(bottom, pc)
|
||||
for c := last.NextSibling(); c != nil; {
|
||||
next := c.NextSibling()
|
||||
|
|
@ -226,21 +246,39 @@ func (s *linkParser) processLinkLabel(parent ast.Node, link *ast.Link, last *lin
|
|||
}
|
||||
}
|
||||
|
||||
func (s *linkParser) parseReferenceLink(parent ast.Node, last *linkLabelState, block text.Reader, pc Context) (*ast.Link, bool) {
|
||||
var linkFindClosureOptions text.FindClosureOptions = text.FindClosureOptions{
|
||||
Nesting: false,
|
||||
Newline: true,
|
||||
Advance: true,
|
||||
}
|
||||
|
||||
func (s *linkParser) parseReferenceLink(parent ast.Node, last *linkLabelState,
|
||||
block text.Reader, pc Context) (*ast.Link, bool) {
|
||||
_, orgpos := block.Position()
|
||||
block.Advance(1) // skip '['
|
||||
line, segment := block.PeekLine()
|
||||
endIndex := util.FindClosure(line, '[', ']', false, true)
|
||||
if endIndex < 0 {
|
||||
segments, found := block.FindClosure('[', ']', linkFindClosureOptions)
|
||||
if !found {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
block.Advance(endIndex + 1)
|
||||
ssegment := segment.WithStop(segment.Start + endIndex)
|
||||
maybeReference := block.Value(ssegment)
|
||||
var maybeReference []byte
|
||||
if segments.Len() == 1 { // avoid allocate a new byte slice
|
||||
maybeReference = block.Value(segments.At(0))
|
||||
} else {
|
||||
maybeReference = []byte{}
|
||||
for i := 0; i < segments.Len(); i++ {
|
||||
s := segments.At(i)
|
||||
maybeReference = append(maybeReference, block.Value(s)...)
|
||||
}
|
||||
}
|
||||
if util.IsBlank(maybeReference) { // collapsed reference link
|
||||
ssegment = text.NewSegment(last.Segment.Stop, orgpos.Start-1)
|
||||
maybeReference = block.Value(ssegment)
|
||||
s := text.NewSegment(last.Segment.Stop, orgpos.Start-1)
|
||||
maybeReference = block.Value(s)
|
||||
}
|
||||
// CommonMark spec says:
|
||||
// > A link label can have at most 999 characters inside the square brackets.
|
||||
if len(maybeReference) > 999 {
|
||||
return nil, true
|
||||
}
|
||||
|
||||
ref, ok := pc.Reference(util.ToLinkReference(maybeReference))
|
||||
|
|
@ -295,20 +333,17 @@ func (s *linkParser) parseLink(parent ast.Node, last *linkLabelState, block text
|
|||
func parseLinkDestination(block text.Reader) ([]byte, bool) {
|
||||
block.SkipSpaces()
|
||||
line, _ := block.PeekLine()
|
||||
buf := []byte{}
|
||||
if block.Peek() == '<' {
|
||||
i := 1
|
||||
for i < len(line) {
|
||||
c := line[i]
|
||||
if c == '\\' && i < len(line)-1 && util.IsPunct(line[i+1]) {
|
||||
buf = append(buf, '\\', line[i+1])
|
||||
i += 2
|
||||
continue
|
||||
} else if c == '>' {
|
||||
block.Advance(i + 1)
|
||||
return line[1:i], true
|
||||
}
|
||||
buf = append(buf, c)
|
||||
i++
|
||||
}
|
||||
return nil, false
|
||||
|
|
@ -318,7 +353,6 @@ func parseLinkDestination(block text.Reader) ([]byte, bool) {
|
|||
for i < len(line) {
|
||||
c := line[i]
|
||||
if c == '\\' && i < len(line)-1 && util.IsPunct(line[i+1]) {
|
||||
buf = append(buf, '\\', line[i+1])
|
||||
i += 2
|
||||
continue
|
||||
} else if c == '(' {
|
||||
|
|
@ -331,7 +365,6 @@ func parseLinkDestination(block text.Reader) ([]byte, bool) {
|
|||
} else if util.IsSpace(c) {
|
||||
break
|
||||
}
|
||||
buf = append(buf, c)
|
||||
i++
|
||||
}
|
||||
block.Advance(i)
|
||||
|
|
@ -348,17 +381,61 @@ func parseLinkTitle(block text.Reader) ([]byte, bool) {
|
|||
if opener == '(' {
|
||||
closer = ')'
|
||||
}
|
||||
line, _ := block.PeekLine()
|
||||
pos := util.FindClosure(line[1:], opener, closer, false, true)
|
||||
if pos < 0 {
|
||||
return nil, false
|
||||
block.Advance(1)
|
||||
segments, found := block.FindClosure(opener, closer, linkFindClosureOptions)
|
||||
if found {
|
||||
if segments.Len() == 1 {
|
||||
return block.Value(segments.At(0)), true
|
||||
}
|
||||
var title []byte
|
||||
for i := 0; i < segments.Len(); i++ {
|
||||
s := segments.At(i)
|
||||
title = append(title, block.Value(s)...)
|
||||
}
|
||||
return title, true
|
||||
}
|
||||
pos += 2 // opener + closer
|
||||
block.Advance(pos)
|
||||
return line[1 : pos-1], true
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func pushLinkBottom(pc Context) {
|
||||
bottoms := pc.Get(linkBottom)
|
||||
b := pc.LastDelimiter()
|
||||
if bottoms == nil {
|
||||
pc.Set(linkBottom, b)
|
||||
return
|
||||
}
|
||||
if s, ok := bottoms.([]ast.Node); ok {
|
||||
pc.Set(linkBottom, append(s, b))
|
||||
return
|
||||
}
|
||||
pc.Set(linkBottom, []ast.Node{bottoms.(ast.Node), b})
|
||||
}
|
||||
|
||||
func popLinkBottom(pc Context) ast.Node {
|
||||
bottoms := pc.Get(linkBottom)
|
||||
if bottoms == nil {
|
||||
return nil
|
||||
}
|
||||
if v, ok := bottoms.(ast.Node); ok {
|
||||
pc.Set(linkBottom, nil)
|
||||
return v
|
||||
}
|
||||
s := bottoms.([]ast.Node)
|
||||
v := s[len(s)-1]
|
||||
n := s[0 : len(s)-1]
|
||||
switch len(n) {
|
||||
case 0:
|
||||
pc.Set(linkBottom, nil)
|
||||
case 1:
|
||||
pc.Set(linkBottom, n[0])
|
||||
default:
|
||||
pc.Set(linkBottom, s[0:len(s)-1])
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (s *linkParser) CloseBlock(parent ast.Node, block text.Reader, pc Context) {
|
||||
pc.Set(linkBottom, nil)
|
||||
tlist := pc.Get(linkLabelStateKey)
|
||||
if tlist == nil {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ func (p *linkReferenceParagraphTransformer) Transform(node *ast.Paragraph, reade
|
|||
|
||||
func parseLinkReferenceDefinition(block text.Reader, pc Context) (int, int) {
|
||||
block.SkipSpaces()
|
||||
line, segment := block.PeekLine()
|
||||
line, _ := block.PeekLine()
|
||||
if line == nil {
|
||||
return -1, -1
|
||||
}
|
||||
|
|
@ -67,39 +67,33 @@ func parseLinkReferenceDefinition(block text.Reader, pc Context) (int, int) {
|
|||
if line[pos] != '[' {
|
||||
return -1, -1
|
||||
}
|
||||
open := segment.Start + pos + 1
|
||||
closes := -1
|
||||
block.Advance(pos + 1)
|
||||
for {
|
||||
line, segment = block.PeekLine()
|
||||
if line == nil {
|
||||
return -1, -1
|
||||
}
|
||||
closure := util.FindClosure(line, '[', ']', false, false)
|
||||
if closure > -1 {
|
||||
closes = segment.Start + closure
|
||||
next := closure + 1
|
||||
if next >= len(line) || line[next] != ':' {
|
||||
return -1, -1
|
||||
}
|
||||
block.Advance(next + 1)
|
||||
break
|
||||
}
|
||||
block.AdvanceLine()
|
||||
}
|
||||
if closes < 0 {
|
||||
segments, found := block.FindClosure('[', ']', linkFindClosureOptions)
|
||||
if !found {
|
||||
return -1, -1
|
||||
}
|
||||
label := block.Value(text.NewSegment(open, closes))
|
||||
var label []byte
|
||||
if segments.Len() == 1 {
|
||||
label = block.Value(segments.At(0))
|
||||
} else {
|
||||
for i := 0; i < segments.Len(); i++ {
|
||||
s := segments.At(i)
|
||||
label = append(label, block.Value(s)...)
|
||||
}
|
||||
}
|
||||
if util.IsBlank(label) {
|
||||
return -1, -1
|
||||
}
|
||||
if block.Peek() != ':' {
|
||||
return -1, -1
|
||||
}
|
||||
block.Advance(1)
|
||||
block.SkipSpaces()
|
||||
destination, ok := parseLinkDestination(block)
|
||||
if !ok {
|
||||
return -1, -1
|
||||
}
|
||||
line, segment = block.PeekLine()
|
||||
line, _ = block.PeekLine()
|
||||
isNewLine := line == nil || util.IsBlank(line)
|
||||
|
||||
endLine, _ := block.Position()
|
||||
|
|
@ -117,45 +111,40 @@ func parseLinkReferenceDefinition(block text.Reader, pc Context) (int, int) {
|
|||
return -1, -1
|
||||
}
|
||||
block.Advance(1)
|
||||
open = -1
|
||||
closes = -1
|
||||
closer := opener
|
||||
if opener == '(' {
|
||||
closer = ')'
|
||||
}
|
||||
for {
|
||||
line, segment = block.PeekLine()
|
||||
if line == nil {
|
||||
segments, found = block.FindClosure(opener, closer, linkFindClosureOptions)
|
||||
if !found {
|
||||
if !isNewLine {
|
||||
return -1, -1
|
||||
}
|
||||
if open < 0 {
|
||||
open = segment.Start
|
||||
}
|
||||
closure := util.FindClosure(line, opener, closer, false, true)
|
||||
if closure > -1 {
|
||||
closes = segment.Start + closure
|
||||
block.Advance(closure + 1)
|
||||
break
|
||||
}
|
||||
ref := NewReference(label, destination, nil)
|
||||
pc.AddReference(ref)
|
||||
block.AdvanceLine()
|
||||
return startLine, endLine + 1
|
||||
}
|
||||
if closes < 0 {
|
||||
return -1, -1
|
||||
var title []byte
|
||||
if segments.Len() == 1 {
|
||||
title = block.Value(segments.At(0))
|
||||
} else {
|
||||
for i := 0; i < segments.Len(); i++ {
|
||||
s := segments.At(i)
|
||||
title = append(title, block.Value(s)...)
|
||||
}
|
||||
}
|
||||
|
||||
line, segment = block.PeekLine()
|
||||
line, _ = block.PeekLine()
|
||||
if line != nil && !util.IsBlank(line) {
|
||||
if !isNewLine {
|
||||
return -1, -1
|
||||
}
|
||||
title := block.Value(text.NewSegment(open, closes))
|
||||
ref := NewReference(label, destination, title)
|
||||
pc.AddReference(ref)
|
||||
return startLine, endLine
|
||||
}
|
||||
|
||||
title := block.Value(text.NewSegment(open, closes))
|
||||
|
||||
endLine, _ = block.Position()
|
||||
ref := NewReference(label, destination, title)
|
||||
pc.AddReference(ref)
|
||||
|
|
|
|||
114
parser/list.go
114
parser/list.go
|
|
@ -1,10 +1,11 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type listItemType int
|
||||
|
|
@ -15,9 +16,13 @@ const (
|
|||
orderedList
|
||||
)
|
||||
|
||||
var skipListParserKey = NewContextKey()
|
||||
var emptyListItemWithBlankLines = NewContextKey()
|
||||
var listItemFlagValue interface{} = true
|
||||
|
||||
// Same as
|
||||
// `^(([ ]*)([\-\*\+]))(\s+.*)?\n?$`.FindSubmatchIndex or
|
||||
// `^(([ ]*)(\d{1,9}[\.\)]))(\s+.*)?\n?$`.FindSubmatchIndex
|
||||
// `^(([ ]*)(\d{1,9}[\.\)]))(\s+.*)?\n?$`.FindSubmatchIndex.
|
||||
func parseListItem(line []byte) ([6]int, listItemType) {
|
||||
i := 0
|
||||
l := len(line)
|
||||
|
|
@ -35,7 +40,7 @@ func parseListItem(line []byte) ([6]int, listItemType) {
|
|||
ret[1] = i
|
||||
ret[2] = i
|
||||
var typ listItemType
|
||||
if i < l && line[i] == '-' || line[i] == '*' || line[i] == '+' {
|
||||
if i < l && (line[i] == '-' || line[i] == '*' || line[i] == '+') {
|
||||
i++
|
||||
ret[3] = i
|
||||
typ = bulletList
|
||||
|
|
@ -46,7 +51,7 @@ func parseListItem(line []byte) ([6]int, listItemType) {
|
|||
if ret[3] == ret[2] || ret[3]-ret[2] > 9 {
|
||||
return ret, notList
|
||||
}
|
||||
if i < l && line[i] == '.' || line[i] == ')' {
|
||||
if i < l && (line[i] == '.' || line[i] == ')') {
|
||||
i++
|
||||
ret[3] = i
|
||||
} else {
|
||||
|
|
@ -56,12 +61,17 @@ func parseListItem(line []byte) ([6]int, listItemType) {
|
|||
} else {
|
||||
return ret, notList
|
||||
}
|
||||
if line[i] != '\n' {
|
||||
if i < l && line[i] != '\n' {
|
||||
w, _ := util.IndentWidth(line[i:], 0)
|
||||
if w == 0 {
|
||||
return ret, notList
|
||||
}
|
||||
}
|
||||
if i >= l {
|
||||
ret[4] = -1
|
||||
ret[5] = -1
|
||||
return ret, typ
|
||||
}
|
||||
ret[4] = i
|
||||
ret[5] = len(line)
|
||||
if line[ret[5]-1] == '\n' && line[i] != '\n' {
|
||||
|
|
@ -79,11 +89,11 @@ func matchesListItem(source []byte, strict bool) ([6]int, listItemType) {
|
|||
}
|
||||
|
||||
func calcListOffset(source []byte, match [6]int) int {
|
||||
offset := 0
|
||||
if util.IsBlank(source[match[4]:]) { // list item starts with a blank line
|
||||
var offset int
|
||||
if match[4] < 0 || util.IsBlank(source[match[4]:]) { // list item starts with a blank line
|
||||
offset = 1
|
||||
} else {
|
||||
offset, _ = util.IndentWidth(source[match[4]:], match[2])
|
||||
offset, _ = util.IndentWidth(source[match[4]:], match[4])
|
||||
if offset > 4 { // offseted codeblock
|
||||
offset = 1
|
||||
}
|
||||
|
|
@ -106,15 +116,19 @@ var defaultListParser = &listParser{}
|
|||
|
||||
// NewListParser returns a new BlockParser that
|
||||
// parses lists.
|
||||
// This parser must take predecence over the ListItemParser.
|
||||
// This parser must take precedence over the ListItemParser.
|
||||
func NewListParser() BlockParser {
|
||||
return defaultListParser
|
||||
}
|
||||
|
||||
func (b *listParser) Trigger() []byte {
|
||||
return []byte{'-', '+', '*', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
|
||||
}
|
||||
|
||||
func (b *listParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||
last := pc.LastOpenedBlock().Node
|
||||
if _, lok := last.(*ast.List); lok || pc.Get(skipListParser) != nil {
|
||||
pc.Set(skipListParser, nil)
|
||||
if _, lok := last.(*ast.List); lok || pc.Get(skipListParserKey) != nil {
|
||||
pc.Set(skipListParserKey, nil)
|
||||
return nil, NoChildren
|
||||
}
|
||||
line, _ := reader.PeekLine()
|
||||
|
|
@ -134,7 +148,7 @@ func (b *listParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.
|
|||
return nil, NoChildren
|
||||
}
|
||||
//an empty list item cannot interrupt a paragraph:
|
||||
if match[5]-match[4] == 1 {
|
||||
if match[4] < 0 || util.IsBlank(line[match[4]:match[5]]) {
|
||||
return nil, NoChildren
|
||||
}
|
||||
}
|
||||
|
|
@ -144,6 +158,7 @@ func (b *listParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.
|
|||
if start > -1 {
|
||||
node.Start = start
|
||||
}
|
||||
pc.Set(emptyListItemWithBlankLines, nil)
|
||||
return node, HasChildren
|
||||
}
|
||||
|
||||
|
|
@ -151,26 +166,11 @@ func (b *listParser) Continue(node ast.Node, reader text.Reader, pc Context) Sta
|
|||
list := node.(*ast.List)
|
||||
line, _ := reader.PeekLine()
|
||||
if util.IsBlank(line) {
|
||||
// A list item can begin with at most one blank line
|
||||
if node.ChildCount() == 1 && node.LastChild().ChildCount() == 0 {
|
||||
return Close
|
||||
if node.LastChild().ChildCount() == 0 {
|
||||
pc.Set(emptyListItemWithBlankLines, listItemFlagValue)
|
||||
}
|
||||
return Continue | HasChildren
|
||||
}
|
||||
// Themantic Breaks take predecence over lists
|
||||
if isThemanticBreak(line) {
|
||||
isHeading := false
|
||||
last := pc.LastOpenedBlock().Node
|
||||
if ast.IsParagraph(last) {
|
||||
c, ok := matchesSetextHeadingBar(line)
|
||||
if ok && c == '-' {
|
||||
isHeading = true
|
||||
}
|
||||
}
|
||||
if !isHeading {
|
||||
return Close
|
||||
}
|
||||
}
|
||||
|
||||
// "offset" means a width that bar indicates.
|
||||
// - aaaaaaaa
|
||||
|
|
@ -180,10 +180,23 @@ func (b *listParser) Continue(node ast.Node, reader text.Reader, pc Context) Sta
|
|||
// - a
|
||||
// - b <--- current line
|
||||
// it maybe a new child of the list.
|
||||
//
|
||||
// Empty list items can have multiple blanklines
|
||||
//
|
||||
// - <--- 1st item is an empty thus "offset" is unknown
|
||||
//
|
||||
//
|
||||
// - <--- current line
|
||||
//
|
||||
// -> 1 list with 2 blank items
|
||||
//
|
||||
// So if the last item is an empty, it maybe a new child of the list.
|
||||
//
|
||||
offset := lastOffset(node)
|
||||
indent, _ := util.IndentWidth(line, 0)
|
||||
lastIsEmpty := node.LastChild().ChildCount() == 0
|
||||
indent, _ := util.IndentWidth(line, reader.LineOffset())
|
||||
|
||||
if indent < offset {
|
||||
if indent < offset || lastIsEmpty {
|
||||
if indent < 4 {
|
||||
match, typ := matchesListItem(line, false) // may have a leading spaces more than 3
|
||||
if typ != notList && match[1]-offset < 4 {
|
||||
|
|
@ -191,9 +204,41 @@ func (b *listParser) Continue(node ast.Node, reader text.Reader, pc Context) Sta
|
|||
if !list.CanContinue(marker, typ == orderedList) {
|
||||
return Close
|
||||
}
|
||||
// Thematic Breaks take precedence over lists
|
||||
if isThematicBreak(line[match[3]-1:], 0) {
|
||||
isHeading := false
|
||||
last := pc.LastOpenedBlock().Node
|
||||
if ast.IsParagraph(last) {
|
||||
c, ok := matchesSetextHeadingBar(line[match[3]-1:])
|
||||
if ok && c == '-' {
|
||||
isHeading = true
|
||||
}
|
||||
}
|
||||
if !isHeading {
|
||||
return Close
|
||||
}
|
||||
}
|
||||
return Continue | HasChildren
|
||||
}
|
||||
}
|
||||
if !lastIsEmpty {
|
||||
return Close
|
||||
}
|
||||
}
|
||||
|
||||
if lastIsEmpty && indent < offset {
|
||||
return Close
|
||||
}
|
||||
|
||||
// Non empty items can not exist next to an empty list item
|
||||
// with blank lines. So we need to close the current list
|
||||
//
|
||||
// -
|
||||
//
|
||||
// foo
|
||||
//
|
||||
// -> 1 list with 1 blank items and 1 paragraph
|
||||
if pc.Get(emptyListItemWithBlankLines) != nil {
|
||||
return Close
|
||||
}
|
||||
return Continue | HasChildren
|
||||
|
|
@ -205,14 +250,14 @@ func (b *listParser) Close(node ast.Node, reader text.Reader, pc Context) {
|
|||
for c := node.FirstChild(); c != nil && list.IsTight; c = c.NextSibling() {
|
||||
if c.FirstChild() != nil && c.FirstChild() != c.LastChild() {
|
||||
for c1 := c.FirstChild().NextSibling(); c1 != nil; c1 = c1.NextSibling() {
|
||||
if bl, ok := c1.(ast.Node); ok && bl.HasBlankPreviousLines() {
|
||||
if c1.HasBlankPreviousLines() {
|
||||
list.IsTight = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if c != node.FirstChild() {
|
||||
if bl, ok := c.(ast.Node); ok && bl.HasBlankPreviousLines() {
|
||||
if c.HasBlankPreviousLines() {
|
||||
list.IsTight = false
|
||||
}
|
||||
}
|
||||
|
|
@ -220,8 +265,9 @@ func (b *listParser) Close(node ast.Node, reader text.Reader, pc Context) {
|
|||
|
||||
if list.IsTight {
|
||||
for child := node.FirstChild(); child != nil; child = child.NextSibling() {
|
||||
for gc := child.FirstChild(); gc != nil; gc = gc.NextSibling() {
|
||||
for gc := child.FirstChild(); gc != nil; {
|
||||
paragraph, ok := gc.(*ast.Paragraph)
|
||||
gc = gc.NextSibling()
|
||||
if ok {
|
||||
textBlock := ast.NewTextBlock()
|
||||
textBlock.SetLines(paragraph.Lines())
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ func NewListItemParser() BlockParser {
|
|||
return defaultListItemParser
|
||||
}
|
||||
|
||||
var skipListParser = NewContextKey()
|
||||
var skipListParserValue interface{} = true
|
||||
func (b *listItemParser) Trigger() []byte {
|
||||
return []byte{'-', '+', '*', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
|
||||
}
|
||||
|
||||
func (b *listItemParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||
list, lok := parent.(*ast.List)
|
||||
|
|
@ -34,9 +35,12 @@ func (b *listItemParser) Open(parent ast.Node, reader text.Reader, pc Context) (
|
|||
if match[1]-offset > 3 {
|
||||
return nil, NoChildren
|
||||
}
|
||||
|
||||
pc.Set(emptyListItemWithBlankLines, nil)
|
||||
|
||||
itemOffset := calcListOffset(line, match)
|
||||
node := ast.NewListItem(match[3] + itemOffset)
|
||||
if match[5]-match[4] == 1 {
|
||||
if match[4] < 0 || util.IsBlank(line[match[4]:match[5]]) {
|
||||
return node, NoChildren
|
||||
}
|
||||
|
||||
|
|
@ -49,18 +53,23 @@ func (b *listItemParser) Open(parent ast.Node, reader text.Reader, pc Context) (
|
|||
func (b *listItemParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||
line, _ := reader.PeekLine()
|
||||
if util.IsBlank(line) {
|
||||
reader.Advance(len(line) - 1)
|
||||
return Continue | HasChildren
|
||||
}
|
||||
|
||||
indent, _ := util.IndentWidth(line, reader.LineOffset())
|
||||
offset := lastOffset(node.Parent())
|
||||
if indent < offset && indent < 4 {
|
||||
isEmpty := node.ChildCount() == 0 && pc.Get(emptyListItemWithBlankLines) != nil
|
||||
indent, _ := util.IndentWidth(line, reader.LineOffset())
|
||||
if (isEmpty || indent < offset) && indent < 4 {
|
||||
_, typ := matchesListItem(line, true)
|
||||
// new list item found
|
||||
if typ != notList {
|
||||
pc.Set(skipListParser, skipListParserValue)
|
||||
pc.Set(skipListParserKey, listItemFlagValue)
|
||||
return Close
|
||||
}
|
||||
if !isEmpty {
|
||||
return Close
|
||||
}
|
||||
return Close
|
||||
}
|
||||
pos, padding := util.IndentPosition(line, reader.LineOffset(), offset)
|
||||
reader.AdvanceAndSetPadding(pos, padding)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package parser
|
|||
import (
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
type paragraphParser struct {
|
||||
|
|
@ -16,6 +17,10 @@ func NewParagraphParser() BlockParser {
|
|||
return defaultParagraphParser
|
||||
}
|
||||
|
||||
func (b *paragraphParser) Trigger() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *paragraphParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||
_, segment := reader.PeekLine()
|
||||
segment = segment.TrimLeftSpace(reader.Source())
|
||||
|
|
@ -29,9 +34,8 @@ func (b *paragraphParser) Open(parent ast.Node, reader text.Reader, pc Context)
|
|||
}
|
||||
|
||||
func (b *paragraphParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||
_, segment := reader.PeekLine()
|
||||
segment = segment.TrimLeftSpace(reader.Source())
|
||||
if segment.IsEmpty() {
|
||||
line, segment := reader.PeekLine()
|
||||
if util.IsBlank(line) {
|
||||
return Close
|
||||
}
|
||||
node.Lines().Append(segment)
|
||||
|
|
@ -42,6 +46,12 @@ func (b *paragraphParser) Continue(node ast.Node, reader text.Reader, pc Context
|
|||
func (b *paragraphParser) Close(node ast.Node, reader text.Reader, pc Context) {
|
||||
lines := node.Lines()
|
||||
if lines.Len() != 0 {
|
||||
// trim leading spaces
|
||||
for i := 0; i < lines.Len(); i++ {
|
||||
l := lines.At(i)
|
||||
lines.Set(i, l.TrimLeftSpace(reader.Source()))
|
||||
}
|
||||
|
||||
// trim trailing spaces
|
||||
length := lines.Len()
|
||||
lastLine := node.Lines().At(length - 1)
|
||||
|
|
|
|||
332
parser/parser.go
332
parser/parser.go
|
|
@ -56,7 +56,7 @@ func (r *reference) String() string {
|
|||
// An IDs interface is a collection of the element ids.
|
||||
type IDs interface {
|
||||
// Generate generates a new element id.
|
||||
Generate(value, prefix []byte) []byte
|
||||
Generate(value []byte, kind ast.NodeKind) []byte
|
||||
|
||||
// Put puts a given element id to the used ids table.
|
||||
Put(value []byte)
|
||||
|
|
@ -72,7 +72,7 @@ func newIDs() IDs {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *ids) Generate(value, prefix []byte) []byte {
|
||||
func (s *ids) Generate(value []byte, kind ast.NodeKind) []byte {
|
||||
value = util.TrimLeftSpace(value)
|
||||
value = util.TrimRightSpace(value)
|
||||
result := []byte{}
|
||||
|
|
@ -84,14 +84,17 @@ func (s *ids) Generate(value, prefix []byte) []byte {
|
|||
continue
|
||||
}
|
||||
if util.IsAlphaNumeric(v) {
|
||||
if 'A' <= v && v <= 'Z' {
|
||||
v += 'a' - 'A'
|
||||
}
|
||||
result = append(result, v)
|
||||
} else if util.IsSpace(v) {
|
||||
} else if util.IsSpace(v) || v == '-' || v == '_' {
|
||||
result = append(result, '-')
|
||||
}
|
||||
}
|
||||
if len(result) == 0 {
|
||||
if prefix != nil {
|
||||
result = append(make([]byte, 0, len(prefix)), prefix...)
|
||||
if kind == ast.KindHeading {
|
||||
result = []byte("heading")
|
||||
} else {
|
||||
result = []byte("id")
|
||||
}
|
||||
|
|
@ -101,7 +104,7 @@ func (s *ids) Generate(value, prefix []byte) []byte {
|
|||
return result
|
||||
}
|
||||
for i := 1; ; i++ {
|
||||
newResult := fmt.Sprintf("%s%d", result, i)
|
||||
newResult := fmt.Sprintf("%s-%d", result, i)
|
||||
if _, ok := s.values[newResult]; !ok {
|
||||
s.values[newResult] = true
|
||||
return []byte(newResult)
|
||||
|
|
@ -114,7 +117,7 @@ func (s *ids) Put(value []byte) {
|
|||
s.values[util.BytesToReadOnlyString(value)] = true
|
||||
}
|
||||
|
||||
// ContextKey is a key that is used to set arbitary values to the context.
|
||||
// ContextKey is a key that is used to set arbitrary values to the context.
|
||||
type ContextKey int
|
||||
|
||||
// ContextKeyMax is a maximum value of the ContextKey.
|
||||
|
|
@ -135,6 +138,9 @@ type Context interface {
|
|||
// Get returns a value associated with the given key.
|
||||
Get(ContextKey) interface{}
|
||||
|
||||
// ComputeIfAbsent computes a value if a value associated with the given key is absent and returns the value.
|
||||
ComputeIfAbsent(ContextKey, func() interface{}) interface{}
|
||||
|
||||
// Set sets the given value to the context.
|
||||
Set(ContextKey, interface{})
|
||||
|
||||
|
|
@ -153,12 +159,22 @@ type Context interface {
|
|||
|
||||
// BlockOffset returns a first non-space character position on current line.
|
||||
// This value is valid only for BlockParser.Open.
|
||||
// BlockOffset returns -1 if current line is blank.
|
||||
BlockOffset() int
|
||||
|
||||
// BlockOffset sets a first non-space character position on current line.
|
||||
// This value is valid only for BlockParser.Open.
|
||||
SetBlockOffset(int)
|
||||
|
||||
// BlockIndent returns an indent width on current line.
|
||||
// This value is valid only for BlockParser.Open.
|
||||
// BlockIndent returns -1 if current line is blank.
|
||||
BlockIndent() int
|
||||
|
||||
// BlockIndent sets an indent width on current line.
|
||||
// This value is valid only for BlockParser.Open.
|
||||
SetBlockIndent(int)
|
||||
|
||||
// FirstDelimiter returns a first delimiter of the current delimiter list.
|
||||
FirstDelimiter() *Delimiter
|
||||
|
||||
|
|
@ -183,6 +199,24 @@ type Context interface {
|
|||
|
||||
// LastOpenedBlock returns a last node that is currently in parsing.
|
||||
LastOpenedBlock() Block
|
||||
|
||||
// IsInLinkLabel returns true if current position seems to be in link label.
|
||||
IsInLinkLabel() bool
|
||||
}
|
||||
|
||||
// A ContextConfig struct is a data structure that holds configuration of the Context.
|
||||
type ContextConfig struct {
|
||||
IDs IDs
|
||||
}
|
||||
|
||||
// An ContextOption is a functional option type for the Context.
|
||||
type ContextOption func(*ContextConfig)
|
||||
|
||||
// WithIDs is a functional option for the Context.
|
||||
func WithIDs(ids IDs) ContextOption {
|
||||
return func(c *ContextConfig) {
|
||||
c.IDs = ids
|
||||
}
|
||||
}
|
||||
|
||||
type parseContext struct {
|
||||
|
|
@ -190,18 +224,27 @@ type parseContext struct {
|
|||
ids IDs
|
||||
refs map[string]Reference
|
||||
blockOffset int
|
||||
blockIndent int
|
||||
delimiters *Delimiter
|
||||
lastDelimiter *Delimiter
|
||||
openedBlocks []Block
|
||||
}
|
||||
|
||||
// NewContext returns a new Context.
|
||||
func NewContext() Context {
|
||||
func NewContext(options ...ContextOption) Context {
|
||||
cfg := &ContextConfig{
|
||||
IDs: newIDs(),
|
||||
}
|
||||
for _, option := range options {
|
||||
option(cfg)
|
||||
}
|
||||
|
||||
return &parseContext{
|
||||
store: make([]interface{}, ContextKeyMax+1),
|
||||
refs: map[string]Reference{},
|
||||
ids: newIDs(),
|
||||
blockOffset: 0,
|
||||
ids: cfg.IDs,
|
||||
blockOffset: -1,
|
||||
blockIndent: -1,
|
||||
delimiters: nil,
|
||||
lastDelimiter: nil,
|
||||
openedBlocks: []Block{},
|
||||
|
|
@ -212,6 +255,15 @@ func (p *parseContext) Get(key ContextKey) interface{} {
|
|||
return p.store[key]
|
||||
}
|
||||
|
||||
func (p *parseContext) ComputeIfAbsent(key ContextKey, f func() interface{}) interface{} {
|
||||
v := p.store[key]
|
||||
if v == nil {
|
||||
v = f()
|
||||
p.store[key] = v
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (p *parseContext) Set(key ContextKey, value interface{}) {
|
||||
p.store[key] = value
|
||||
}
|
||||
|
|
@ -228,6 +280,14 @@ func (p *parseContext) SetBlockOffset(v int) {
|
|||
p.blockOffset = v
|
||||
}
|
||||
|
||||
func (p *parseContext) BlockIndent() int {
|
||||
return p.blockIndent
|
||||
}
|
||||
|
||||
func (p *parseContext) SetBlockIndent(v int) {
|
||||
p.blockIndent = v
|
||||
}
|
||||
|
||||
func (p *parseContext) LastDelimiter() *Delimiter {
|
||||
return p.lastDelimiter
|
||||
}
|
||||
|
|
@ -333,12 +393,18 @@ func (p *parseContext) LastOpenedBlock() Block {
|
|||
return Block{}
|
||||
}
|
||||
|
||||
func (p *parseContext) IsInLinkLabel() bool {
|
||||
tlist := p.Get(linkLabelStateKey)
|
||||
return tlist != nil
|
||||
}
|
||||
|
||||
// State represents parser's state.
|
||||
// State is designed to use as a bit flag.
|
||||
type State int
|
||||
|
||||
const (
|
||||
none State = 1 << iota
|
||||
// None is a default value of the [State].
|
||||
None State = 1 << iota
|
||||
|
||||
// Continue indicates parser can continue parsing.
|
||||
Continue
|
||||
|
|
@ -351,6 +417,11 @@ const (
|
|||
|
||||
// NoChildren indicates parser does not have child blocks.
|
||||
NoChildren
|
||||
|
||||
// RequireParagraph indicates parser requires that the last node
|
||||
// must be a paragraph and is not converted to other nodes by
|
||||
// ParagraphTransformers.
|
||||
RequireParagraph
|
||||
)
|
||||
|
||||
// A Config struct is a data structure that holds configuration of the Parser.
|
||||
|
|
@ -360,6 +431,7 @@ type Config struct {
|
|||
InlineParsers util.PrioritizedSlice /*<InlineParser>*/
|
||||
ParagraphTransformers util.PrioritizedSlice /*<ParagraphTransformer>*/
|
||||
ASTTransformers util.PrioritizedSlice /*<ASTTransformer>*/
|
||||
EscapedSpace bool
|
||||
}
|
||||
|
||||
// NewConfig returns a new Config.
|
||||
|
|
@ -401,7 +473,7 @@ type Parser interface {
|
|||
// Parse parses the given Markdown text into AST nodes.
|
||||
Parse(reader text.Reader, opts ...ParseOption) ast.Node
|
||||
|
||||
// AddOption adds the given option to thie parser.
|
||||
// AddOption adds the given option to this parser.
|
||||
AddOptions(...Option)
|
||||
}
|
||||
|
||||
|
|
@ -416,6 +488,11 @@ type SetOptioner interface {
|
|||
// A BlockParser interface parses a block level element like Paragraph, List,
|
||||
// Blockquote etc.
|
||||
type BlockParser interface {
|
||||
// Trigger returns a list of characters that triggers Parse method of
|
||||
// this parser.
|
||||
// If Trigger returns a nil, Open will be called with any lines.
|
||||
Trigger() []byte
|
||||
|
||||
// Open parses the current line and returns a result of parsing.
|
||||
//
|
||||
// Open must not parse beyond the current line.
|
||||
|
|
@ -442,7 +519,7 @@ type BlockParser interface {
|
|||
// Close will be called when the parser returns Close.
|
||||
Close(node ast.Node, reader text.Reader, pc Context)
|
||||
|
||||
// CanInterruptParagraph returns true if the parser can interrupt pargraphs,
|
||||
// CanInterruptParagraph returns true if the parser can interrupt paragraphs,
|
||||
// otherwise false.
|
||||
CanInterruptParagraph() bool
|
||||
|
||||
|
|
@ -491,20 +568,20 @@ type ASTTransformer interface {
|
|||
// DefaultBlockParsers returns a new list of default BlockParsers.
|
||||
// Priorities of default BlockParsers are:
|
||||
//
|
||||
// SetextHeadingParser, 100
|
||||
// ThemanticBreakParser, 200
|
||||
// ListParser, 300
|
||||
// ListItemParser, 400
|
||||
// CodeBlockParser, 500
|
||||
// ATXHeadingParser, 600
|
||||
// FencedCodeBlockParser, 700
|
||||
// BlockquoteParser, 800
|
||||
// HTMLBlockParser, 900
|
||||
// ParagraphParser, 1000
|
||||
// SetextHeadingParser, 100
|
||||
// ThematicBreakParser, 200
|
||||
// ListParser, 300
|
||||
// ListItemParser, 400
|
||||
// CodeBlockParser, 500
|
||||
// ATXHeadingParser, 600
|
||||
// FencedCodeBlockParser, 700
|
||||
// BlockquoteParser, 800
|
||||
// HTMLBlockParser, 900
|
||||
// ParagraphParser, 1000
|
||||
func DefaultBlockParsers() []util.PrioritizedValue {
|
||||
return []util.PrioritizedValue{
|
||||
util.Prioritized(NewSetextHeadingParser(), 100),
|
||||
util.Prioritized(NewThemanticBreakParser(), 200),
|
||||
util.Prioritized(NewThematicBreakParser(), 200),
|
||||
util.Prioritized(NewListParser(), 300),
|
||||
util.Prioritized(NewListItemParser(), 400),
|
||||
util.Prioritized(NewCodeBlockParser(), 500),
|
||||
|
|
@ -519,11 +596,11 @@ func DefaultBlockParsers() []util.PrioritizedValue {
|
|||
// DefaultInlineParsers returns a new list of default InlineParsers.
|
||||
// Priorities of default InlineParsers are:
|
||||
//
|
||||
// CodeSpanParser, 100
|
||||
// LinkParser, 200
|
||||
// AutoLinkParser, 300
|
||||
// RawHTMLParser, 400
|
||||
// EmphasisParser, 500
|
||||
// CodeSpanParser, 100
|
||||
// LinkParser, 200
|
||||
// AutoLinkParser, 300
|
||||
// RawHTMLParser, 400
|
||||
// EmphasisParser, 500
|
||||
func DefaultInlineParsers() []util.PrioritizedValue {
|
||||
return []util.PrioritizedValue{
|
||||
util.Prioritized(NewCodeSpanParser(), 100),
|
||||
|
|
@ -537,7 +614,7 @@ func DefaultInlineParsers() []util.PrioritizedValue {
|
|||
// DefaultParagraphTransformers returns a new list of default ParagraphTransformers.
|
||||
// Priorities of default ParagraphTransformers are:
|
||||
//
|
||||
// LinkReferenceParagraphTransformer, 100
|
||||
// LinkReferenceParagraphTransformer, 100
|
||||
func DefaultParagraphTransformers() []util.PrioritizedValue {
|
||||
return []util.PrioritizedValue{
|
||||
util.Prioritized(LinkReferenceParagraphTransformer, 100),
|
||||
|
|
@ -554,11 +631,13 @@ type Block struct {
|
|||
|
||||
type parser struct {
|
||||
options map[OptionName]interface{}
|
||||
blockParsers []BlockParser
|
||||
blockParsers [256][]BlockParser
|
||||
freeBlockParsers []BlockParser
|
||||
inlineParsers [256][]InlineParser
|
||||
closeBlockers []CloseBlocker
|
||||
paragraphTransformers []ParagraphTransformer
|
||||
astTransformers []ASTTransformer
|
||||
escapedSpace bool
|
||||
config *Config
|
||||
initSync sync.Once
|
||||
}
|
||||
|
|
@ -619,6 +698,18 @@ func WithASTTransformers(ps ...util.PrioritizedValue) Option {
|
|||
return &withASTTransformers{ps}
|
||||
}
|
||||
|
||||
type withEscapedSpace struct {
|
||||
}
|
||||
|
||||
func (o *withEscapedSpace) SetParserOption(c *Config) {
|
||||
c.EscapedSpace = true
|
||||
}
|
||||
|
||||
// WithEscapedSpace is a functional option indicates that a '\' escaped half-space(0x20) should not trigger parsers.
|
||||
func WithEscapedSpace() Option {
|
||||
return &withEscapedSpace{}
|
||||
}
|
||||
|
||||
type withOption struct {
|
||||
name OptionName
|
||||
value interface{}
|
||||
|
|
@ -629,7 +720,7 @@ func (o *withOption) SetParserOption(c *Config) {
|
|||
}
|
||||
|
||||
// WithOption is a functional option that allow you to set
|
||||
// an arbitary option to the parser.
|
||||
// an arbitrary option to the parser.
|
||||
func WithOption(name OptionName, value interface{}) Option {
|
||||
return &withOption{name, value}
|
||||
}
|
||||
|
|
@ -660,13 +751,23 @@ func (p *parser) addBlockParser(v util.PrioritizedValue, options map[OptionName]
|
|||
if !ok {
|
||||
panic(fmt.Sprintf("%v is not a BlockParser", v.Value))
|
||||
}
|
||||
tcs := bp.Trigger()
|
||||
so, ok := v.Value.(SetOptioner)
|
||||
if ok {
|
||||
for oname, ovalue := range options {
|
||||
so.SetOption(oname, ovalue)
|
||||
}
|
||||
}
|
||||
p.blockParsers = append(p.blockParsers, bp)
|
||||
if tcs == nil {
|
||||
p.freeBlockParsers = append(p.freeBlockParsers, bp)
|
||||
} else {
|
||||
for _, tc := range tcs {
|
||||
if p.blockParsers[tc] == nil {
|
||||
p.blockParsers[tc] = []BlockParser{}
|
||||
}
|
||||
p.blockParsers[tc] = append(p.blockParsers[tc], bp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) addInlineParser(v util.PrioritizedValue, options map[OptionName]interface{}) {
|
||||
|
|
@ -742,6 +843,12 @@ func (p *parser) Parse(reader text.Reader, opts ...ParseOption) ast.Node {
|
|||
for _, v := range p.config.BlockParsers {
|
||||
p.addBlockParser(v, p.config.Options)
|
||||
}
|
||||
for i := range p.blockParsers {
|
||||
if p.blockParsers[i] != nil {
|
||||
p.blockParsers[i] = append(p.blockParsers[i], p.freeBlockParsers...)
|
||||
}
|
||||
}
|
||||
|
||||
p.config.InlineParsers.Sort()
|
||||
for _, v := range p.config.InlineParsers {
|
||||
p.addInlineParser(v, p.config.Options)
|
||||
|
|
@ -754,6 +861,7 @@ func (p *parser) Parse(reader text.Reader, opts ...ParseOption) ast.Node {
|
|||
for _, v := range p.config.ASTTransformers {
|
||||
p.addASTTransformer(v, p.config.Options)
|
||||
}
|
||||
p.escapedSpace = p.config.EscapedSpace
|
||||
p.config = nil
|
||||
})
|
||||
c := &ParseConfig{}
|
||||
|
|
@ -766,6 +874,7 @@ func (p *parser) Parse(reader text.Reader, opts ...ParseOption) ast.Node {
|
|||
pc := c.Context
|
||||
root := ast.NewDocument()
|
||||
p.parseBlocks(root, reader, pc)
|
||||
|
||||
blockReader := text.NewBlockReader(reader.Source(), nil)
|
||||
p.walkBlock(root, func(node ast.Node) {
|
||||
p.parseBlock(blockReader, node, pc)
|
||||
|
|
@ -773,28 +882,32 @@ func (p *parser) Parse(reader text.Reader, opts ...ParseOption) ast.Node {
|
|||
for _, at := range p.astTransformers {
|
||||
at.Transform(root, reader, pc)
|
||||
}
|
||||
//root.Dump(reader.Source(), 0)
|
||||
|
||||
// root.Dump(reader.Source(), 0)
|
||||
return root
|
||||
}
|
||||
|
||||
func (p *parser) transformParagraph(node *ast.Paragraph, reader text.Reader, pc Context) {
|
||||
func (p *parser) transformParagraph(node *ast.Paragraph, reader text.Reader, pc Context) bool {
|
||||
for _, pt := range p.paragraphTransformers {
|
||||
pt.Transform(node, reader, pc)
|
||||
if node.Parent() == nil {
|
||||
break
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *parser) closeBlocks(from, to int, reader text.Reader, pc Context) {
|
||||
blocks := pc.OpenedBlocks()
|
||||
for i := from; i >= to; i-- {
|
||||
node := blocks[i].Node
|
||||
blocks[i].Parser.Close(blocks[i].Node, reader, pc)
|
||||
paragraph, ok := node.(*ast.Paragraph)
|
||||
if ok && node.Parent() != nil {
|
||||
p.transformParagraph(paragraph, reader, pc)
|
||||
}
|
||||
if node.Parent() != nil { // closes only if node has not been transformed
|
||||
blocks[i].Parser.Close(blocks[i].Node, reader, pc)
|
||||
}
|
||||
}
|
||||
if from == len(blocks)-1 {
|
||||
blocks = blocks[0:to]
|
||||
|
|
@ -820,35 +933,69 @@ func (p *parser) openBlocks(parent ast.Node, blankLine bool, reader text.Reader,
|
|||
continuable = ast.IsParagraph(lastBlock.Node)
|
||||
}
|
||||
retry:
|
||||
shouldPeek := true
|
||||
//var currentLineNum int
|
||||
var w int
|
||||
var pos int
|
||||
var line []byte
|
||||
for _, bp := range p.blockParsers {
|
||||
if shouldPeek {
|
||||
//currentLineNum, _ = reader.Position()
|
||||
line, _ = reader.PeekLine()
|
||||
w, pos = util.IndentWidth(line, 0)
|
||||
pc.SetBlockOffset(pos)
|
||||
shouldPeek = false
|
||||
if line == nil || line[0] == '\n' {
|
||||
break
|
||||
}
|
||||
var bps []BlockParser
|
||||
line, _ := reader.PeekLine()
|
||||
w, pos := util.IndentWidth(line, reader.LineOffset())
|
||||
if w >= len(line) {
|
||||
pc.SetBlockOffset(-1)
|
||||
pc.SetBlockIndent(-1)
|
||||
} else {
|
||||
pc.SetBlockOffset(pos)
|
||||
pc.SetBlockIndent(w)
|
||||
}
|
||||
if line == nil || line[0] == '\n' {
|
||||
goto continuable
|
||||
}
|
||||
bps = p.freeBlockParsers
|
||||
if pos < len(line) {
|
||||
bps = p.blockParsers[line[pos]]
|
||||
if bps == nil {
|
||||
bps = p.freeBlockParsers
|
||||
}
|
||||
}
|
||||
if bps == nil {
|
||||
goto continuable
|
||||
}
|
||||
|
||||
for _, bp := range bps {
|
||||
if continuable && result == noBlocksOpened && !bp.CanInterruptParagraph() {
|
||||
continue
|
||||
}
|
||||
if w > 3 && !bp.CanAcceptIndentedLine() {
|
||||
continue
|
||||
}
|
||||
last := pc.LastOpenedBlock().Node
|
||||
lastBlock = pc.LastOpenedBlock()
|
||||
last := lastBlock.Node
|
||||
node, state := bp.Open(parent, reader, pc)
|
||||
// if l, _ := reader.Position(); l != currentLineNum {
|
||||
// panic("BlockParser.Open must not advance position beyond the current line")
|
||||
// }
|
||||
if node != nil {
|
||||
shouldPeek = true
|
||||
// Parser requires last node to be a paragraph.
|
||||
// With table extension:
|
||||
//
|
||||
// 0
|
||||
// -:
|
||||
// -
|
||||
//
|
||||
// '-' on 3rd line seems a Setext heading because 1st and 2nd lines
|
||||
// are being paragraph when the Settext heading parser tries to parse the 3rd
|
||||
// line.
|
||||
// But 1st line and 2nd line are a table. Thus this paragraph will be transformed
|
||||
// by a paragraph transformer. So this text should be converted to a table and
|
||||
// an empty list.
|
||||
if state&RequireParagraph != 0 {
|
||||
if last == parent.LastChild() {
|
||||
// Opened paragraph may be transformed by ParagraphTransformers in
|
||||
// closeBlocks().
|
||||
lastBlock.Parser.Close(last, reader, pc)
|
||||
blocks := pc.OpenedBlocks()
|
||||
pc.SetOpenedBlocks(blocks[0 : len(blocks)-1])
|
||||
if p.transformParagraph(last.(*ast.Paragraph), reader, pc) {
|
||||
// Paragraph has been transformed.
|
||||
// So this parser is considered as failing.
|
||||
continuable = false
|
||||
goto retry
|
||||
}
|
||||
}
|
||||
}
|
||||
node.SetBlankPreviousLines(blankLine)
|
||||
if last != nil && last.Parent() == nil {
|
||||
lastPos := len(pc.OpenedBlocks()) - 1
|
||||
|
|
@ -858,13 +1005,15 @@ retry:
|
|||
result = newBlocksOpened
|
||||
be := Block{node, bp}
|
||||
pc.SetOpenedBlocks(append(pc.OpenedBlocks(), be))
|
||||
if state == HasChildren {
|
||||
if state&HasChildren != 0 {
|
||||
parent = node
|
||||
goto retry // try child block
|
||||
}
|
||||
break // no children, can not open more blocks on this line
|
||||
}
|
||||
}
|
||||
|
||||
continuable:
|
||||
if result == noBlocksOpened && continuable {
|
||||
state := lastBlock.Parser.Continue(lastBlock.Node, reader, pc)
|
||||
if state&Continue != 0 {
|
||||
|
|
@ -881,8 +1030,9 @@ type lineStat struct {
|
|||
}
|
||||
|
||||
func isBlankLine(lineNum, level int, stats []lineStat) bool {
|
||||
ret := false
|
||||
ret := true
|
||||
for i := len(stats) - 1 - level; i >= 0; i-- {
|
||||
ret = false
|
||||
s := stats[i]
|
||||
if s.lineNum == lineNum {
|
||||
if s.level < level && s.isBlank {
|
||||
|
|
@ -901,7 +1051,7 @@ func isBlankLine(lineNum, level int, stats []lineStat) bool {
|
|||
func (p *parser) parseBlocks(parent ast.Node, reader text.Reader, pc Context) {
|
||||
pc.SetOpenedBlocks([]Block{})
|
||||
blankLines := make([]lineStat, 0, 128)
|
||||
isBlank := false
|
||||
var isBlank bool
|
||||
for { // process blocks separated by blank lines
|
||||
_, lines, ok := reader.SkipBlankLines()
|
||||
if !ok {
|
||||
|
|
@ -959,8 +1109,14 @@ func (p *parser) parseBlocks(parent ast.Node, reader text.Reader, pc Context) {
|
|||
if i != 0 {
|
||||
thisParent = openedBlocks[i-1].Node
|
||||
}
|
||||
lastNode := openedBlocks[lastIndex].Node
|
||||
result := p.openBlocks(thisParent, isBlank, reader, pc)
|
||||
if result != paragraphContinuation {
|
||||
// lastNode is a paragraph and was transformed by the paragraph
|
||||
// transformers.
|
||||
if openedBlocks[lastIndex].Node != lastNode {
|
||||
lastIndex--
|
||||
}
|
||||
p.closeBlocks(lastIndex, i, reader, pc)
|
||||
}
|
||||
break
|
||||
|
|
@ -978,6 +1134,12 @@ func (p *parser) walkBlock(block ast.Node, cb func(node ast.Node)) {
|
|||
cb(block)
|
||||
}
|
||||
|
||||
const (
|
||||
lineBreakHard uint8 = 1 << iota
|
||||
lineBreakSoft
|
||||
lineBreakVisible
|
||||
)
|
||||
|
||||
func (p *parser) parseBlock(block text.BlockReader, parent ast.Node, pc Context) {
|
||||
if parent.IsRaw() {
|
||||
return
|
||||
|
|
@ -992,18 +1154,42 @@ func (p *parser) parseBlock(block text.BlockReader, parent ast.Node, pc Context)
|
|||
break
|
||||
}
|
||||
lineLength := len(line)
|
||||
var lineBreakFlags uint8
|
||||
hasNewLine := line[lineLength-1] == '\n'
|
||||
if ((lineLength >= 3 && line[lineLength-2] == '\\' &&
|
||||
line[lineLength-3] != '\\') || (lineLength == 2 && line[lineLength-2] == '\\')) && hasNewLine { // ends with \\n
|
||||
lineLength -= 2
|
||||
lineBreakFlags |= lineBreakHard | lineBreakVisible
|
||||
} else if ((lineLength >= 4 && line[lineLength-3] == '\\' && line[lineLength-2] == '\r' &&
|
||||
line[lineLength-4] != '\\') || (lineLength == 3 && line[lineLength-3] == '\\' && line[lineLength-2] == '\r')) &&
|
||||
hasNewLine { // ends with \\r\n
|
||||
lineLength -= 3
|
||||
lineBreakFlags |= lineBreakHard | lineBreakVisible
|
||||
} else if lineLength >= 3 && line[lineLength-3] == ' ' && line[lineLength-2] == ' ' &&
|
||||
hasNewLine { // ends with [space][space]\n
|
||||
lineLength -= 3
|
||||
lineBreakFlags |= lineBreakHard
|
||||
} else if lineLength >= 4 && line[lineLength-4] == ' ' && line[lineLength-3] == ' ' &&
|
||||
line[lineLength-2] == '\r' && hasNewLine { // ends with [space][space]\r\n
|
||||
lineLength -= 4
|
||||
lineBreakFlags |= lineBreakHard
|
||||
} else if hasNewLine {
|
||||
// If the line ends with a newline character, but it is not a hardlineBreak, then it is a softLinebreak
|
||||
// If the line ends with a hardlineBreak, then it cannot end with a softLinebreak
|
||||
// See https://spec.commonmark.org/0.30/#soft-line-breaks
|
||||
lineBreakFlags |= lineBreakSoft
|
||||
}
|
||||
|
||||
l, startPosition := block.Position()
|
||||
n := 0
|
||||
softLinebreak := false
|
||||
for i := 0; i < lineLength; i++ {
|
||||
c := line[i]
|
||||
if c == '\n' {
|
||||
softLinebreak = true
|
||||
break
|
||||
}
|
||||
isSpace := util.IsSpace(c)
|
||||
isSpace := util.IsSpace(c) && c != '\r' && c != '\n'
|
||||
isPunct := util.IsPunct(c)
|
||||
if (isPunct && !escaped) || isSpace || i == 0 {
|
||||
if (isPunct && !escaped) || isSpace && !(escaped && p.escapedSpace) || i == 0 {
|
||||
parserChar := c
|
||||
if isSpace || (i == 0 && !isPunct) {
|
||||
parserChar = ' '
|
||||
|
|
@ -1055,18 +1241,14 @@ func (p *parser) parseBlock(block text.BlockReader, parent ast.Node, pc Context)
|
|||
continue
|
||||
}
|
||||
diff := startPosition.Between(currentPosition)
|
||||
stop := diff.Stop
|
||||
hardlineBreak := false
|
||||
if lineLength > 2 && line[lineLength-2] == '\\' && softLinebreak { // ends with \\n
|
||||
stop--
|
||||
hardlineBreak = true
|
||||
} else if lineLength > 3 && line[lineLength-3] == ' ' && line[lineLength-2] == ' ' && softLinebreak { // ends with [space][space]\n
|
||||
hardlineBreak = true
|
||||
var text *ast.Text
|
||||
if lineBreakFlags&(lineBreakHard|lineBreakVisible) == lineBreakHard|lineBreakVisible {
|
||||
text = ast.NewTextSegment(diff)
|
||||
} else {
|
||||
text = ast.NewTextSegment(diff.TrimRightSpace(source))
|
||||
}
|
||||
rest := diff.WithStop(stop)
|
||||
text := ast.NewTextSegment(rest.TrimRightSpace(source))
|
||||
text.SetSoftLineBreak(softLinebreak)
|
||||
text.SetHardLineBreak(hardlineBreak)
|
||||
text.SetSoftLineBreak(lineBreakFlags&lineBreakSoft != 0)
|
||||
text.SetHardLineBreak(lineBreakFlags&lineBreakHard != 0)
|
||||
parent.AppendChild(parent, text)
|
||||
block.AdvanceLine()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,24 +2,22 @@ package parser
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
type rawHTMLParser struct {
|
||||
HTMLConfig
|
||||
}
|
||||
|
||||
var defaultRawHTMLParser = &rawHTMLParser{}
|
||||
|
||||
// NewRawHTMLParser return a new InlineParser that can parse
|
||||
// inline htmls
|
||||
func NewRawHTMLParser(opts ...HTMLOption) InlineParser {
|
||||
p := &rawHTMLParser{}
|
||||
for _, o := range opts {
|
||||
o.SetHTMLOption(&p.HTMLConfig)
|
||||
}
|
||||
return p
|
||||
// inline htmls.
|
||||
func NewRawHTMLParser() InlineParser {
|
||||
return defaultRawHTMLParser
|
||||
}
|
||||
|
||||
func (s *rawHTMLParser) Trigger() []byte {
|
||||
|
|
@ -34,62 +32,96 @@ func (s *rawHTMLParser) Parse(parent ast.Node, block text.Reader, pc Context) as
|
|||
if len(line) > 2 && line[1] == '/' && util.IsAlphaNumeric(line[2]) {
|
||||
return s.parseMultiLineRegexp(closeTagRegexp, block, pc)
|
||||
}
|
||||
if bytes.HasPrefix(line, []byte("<!--")) {
|
||||
return s.parseMultiLineRegexp(commentRegexp, block, pc)
|
||||
if bytes.HasPrefix(line, openComment) {
|
||||
return s.parseComment(block, pc)
|
||||
}
|
||||
if bytes.HasPrefix(line, []byte("<?")) {
|
||||
return s.parseSingleLineRegexp(processingInstructionRegexp, block, pc)
|
||||
if bytes.HasPrefix(line, openProcessingInstruction) {
|
||||
return s.parseUntil(block, closeProcessingInstruction, pc)
|
||||
}
|
||||
if len(line) > 2 && line[1] == '!' && line[2] >= 'A' && line[2] <= 'Z' {
|
||||
return s.parseSingleLineRegexp(declRegexp, block, pc)
|
||||
return s.parseUntil(block, closeDecl, pc)
|
||||
}
|
||||
if bytes.HasPrefix(line, []byte("<![CDATA[")) {
|
||||
return s.parseMultiLineRegexp(cdataRegexp, block, pc)
|
||||
if bytes.HasPrefix(line, openCDATA) {
|
||||
return s.parseUntil(block, closeCDATA, pc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var tagnamePattern = `([A-Za-z][A-Za-z0-9-]*)`
|
||||
var attributePattern = `(?:\s+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:\s*=\s*(?:[^\"'=<>` + "`" + `\x00-\x20]+|'[^']*'|"[^"]*"))?)`
|
||||
var openTagRegexp = regexp.MustCompile("^<" + tagnamePattern + attributePattern + `*\s*/?>`)
|
||||
var closeTagRegexp = regexp.MustCompile("^</" + tagnamePattern + `\s*>`)
|
||||
var commentRegexp = regexp.MustCompile(`^<!---->|<!--(?:-?[^>-])(?:-?[^-])*-->`)
|
||||
var processingInstructionRegexp = regexp.MustCompile(`^(?:<\?).*?(?:\?>)`)
|
||||
var declRegexp = regexp.MustCompile(`^<![A-Z]+\s+[^>]*>`)
|
||||
var cdataRegexp = regexp.MustCompile(`<!\[CDATA\[[\s\S]*?\]\]>`)
|
||||
var spaceOrOneNewline = `(?:[ \t]|(?:\r\n|\n){0,1})`
|
||||
var attributePattern = `(?:[\r\n \t]+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:[\r\n \t]*=[\r\n \t]*(?:[^\"'=<>` + "`" + `\x00-\x20]+|'[^']*'|"[^"]*"))?)` //nolint:golint,lll
|
||||
var openTagRegexp = regexp.MustCompile("^<" + tagnamePattern + attributePattern + `*` + spaceOrOneNewline + `*/?>`)
|
||||
var closeTagRegexp = regexp.MustCompile("^</" + tagnamePattern + spaceOrOneNewline + `*>`)
|
||||
|
||||
func (s *rawHTMLParser) parseSingleLineRegexp(reg *regexp.Regexp, block text.Reader, pc Context) ast.Node {
|
||||
line, segment := block.PeekLine()
|
||||
match := reg.FindSubmatchIndex(line)
|
||||
if match == nil {
|
||||
return nil
|
||||
}
|
||||
var openProcessingInstruction = []byte("<?")
|
||||
var closeProcessingInstruction = []byte("?>")
|
||||
var openCDATA = []byte("<![CDATA[")
|
||||
var closeCDATA = []byte("]]>")
|
||||
var closeDecl = []byte(">")
|
||||
var emptyComment1 = []byte("<!-->")
|
||||
var emptyComment2 = []byte("<!--->")
|
||||
var openComment = []byte("<!--")
|
||||
var closeComment = []byte("-->")
|
||||
|
||||
func (s *rawHTMLParser) parseComment(block text.Reader, pc Context) ast.Node {
|
||||
savedLine, savedSegment := block.Position()
|
||||
node := ast.NewRawHTML()
|
||||
node.Segments.Append(segment.WithStop(segment.Start + match[1]))
|
||||
block.Advance(match[1])
|
||||
return node
|
||||
line, segment := block.PeekLine()
|
||||
if bytes.HasPrefix(line, emptyComment1) {
|
||||
node.Segments.Append(segment.WithStop(segment.Start + len(emptyComment1)))
|
||||
block.Advance(len(emptyComment1))
|
||||
return node
|
||||
}
|
||||
if bytes.HasPrefix(line, emptyComment2) {
|
||||
node.Segments.Append(segment.WithStop(segment.Start + len(emptyComment2)))
|
||||
block.Advance(len(emptyComment2))
|
||||
return node
|
||||
}
|
||||
offset := len(openComment)
|
||||
line = line[offset:]
|
||||
for {
|
||||
index := bytes.Index(line, closeComment)
|
||||
if index > -1 {
|
||||
node.Segments.Append(segment.WithStop(segment.Start + offset + index + len(closeComment)))
|
||||
block.Advance(offset + index + len(closeComment))
|
||||
return node
|
||||
}
|
||||
offset = 0
|
||||
node.Segments.Append(segment)
|
||||
block.AdvanceLine()
|
||||
line, segment = block.PeekLine()
|
||||
if line == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
block.SetPosition(savedLine, savedSegment)
|
||||
return nil
|
||||
}
|
||||
|
||||
var dummyMatch = [][]byte{}
|
||||
func (s *rawHTMLParser) parseUntil(block text.Reader, closer []byte, pc Context) ast.Node {
|
||||
savedLine, savedSegment := block.Position()
|
||||
node := ast.NewRawHTML()
|
||||
for {
|
||||
line, segment := block.PeekLine()
|
||||
if line == nil {
|
||||
break
|
||||
}
|
||||
index := bytes.Index(line, closer)
|
||||
if index > -1 {
|
||||
node.Segments.Append(segment.WithStop(segment.Start + index + len(closer)))
|
||||
block.Advance(index + len(closer))
|
||||
return node
|
||||
}
|
||||
node.Segments.Append(segment)
|
||||
block.AdvanceLine()
|
||||
}
|
||||
block.SetPosition(savedLine, savedSegment)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *rawHTMLParser) parseMultiLineRegexp(reg *regexp.Regexp, block text.Reader, pc Context) ast.Node {
|
||||
sline, ssegment := block.Position()
|
||||
var m [][]byte
|
||||
if s.FilterTags != nil {
|
||||
m = block.FindSubMatch(reg)
|
||||
} else {
|
||||
if block.Match(reg) {
|
||||
m = dummyMatch
|
||||
}
|
||||
}
|
||||
|
||||
if m != nil {
|
||||
if s.FilterTags != nil {
|
||||
tagName := string(m[1])
|
||||
if _, ok := s.FilterTags[tagName]; ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if block.Match(reg) {
|
||||
node := ast.NewRawHTML()
|
||||
eline, esegment := block.Position()
|
||||
block.SetPosition(sline, ssegment)
|
||||
|
|
@ -112,15 +144,10 @@ func (s *rawHTMLParser) parseMultiLineRegexp(reg *regexp.Regexp, block text.Read
|
|||
if l == eline {
|
||||
block.Advance(end - start)
|
||||
break
|
||||
} else {
|
||||
block.AdvanceLine()
|
||||
}
|
||||
block.AdvanceLine()
|
||||
}
|
||||
return node
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *rawHTMLParser) CloseBlock(parent ast.Node, pc Context) {
|
||||
// nothing to do
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ func NewSetextHeadingParser(opts ...HeadingOption) BlockParser {
|
|||
return p
|
||||
}
|
||||
|
||||
func (b *setextHeadingParser) Trigger() []byte {
|
||||
return []byte{'-', '='}
|
||||
}
|
||||
|
||||
func (b *setextHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||
last := pc.LastOpenedBlock().Node
|
||||
if last == nil {
|
||||
|
|
@ -65,8 +69,8 @@ func (b *setextHeadingParser) Open(parent ast.Node, reader text.Reader, pc Conte
|
|||
}
|
||||
node := ast.NewHeading(level)
|
||||
node.Lines().Append(segment)
|
||||
pc.Set(temporaryParagraphKey, paragraph)
|
||||
return node, NoChildren
|
||||
pc.Set(temporaryParagraphKey, last)
|
||||
return node, NoChildren | RequireParagraph
|
||||
}
|
||||
|
||||
func (b *setextHeadingParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||
|
|
@ -87,34 +91,28 @@ func (b *setextHeadingParser) Close(node ast.Node, reader text.Reader, pc Contex
|
|||
para.Lines().Append(segment)
|
||||
heading.Parent().InsertAfter(heading.Parent(), heading, para)
|
||||
} else {
|
||||
next.(ast.Node).Lines().Unshift(segment)
|
||||
next.Lines().Unshift(segment)
|
||||
}
|
||||
heading.Parent().RemoveChild(heading.Parent(), heading)
|
||||
} else {
|
||||
heading.SetLines(tmp.Lines())
|
||||
heading.SetBlankPreviousLines(tmp.HasBlankPreviousLines())
|
||||
tmp.Parent().RemoveChild(tmp.Parent(), tmp)
|
||||
}
|
||||
|
||||
if b.Attribute {
|
||||
lastIndex := node.Lines().Len() - 1
|
||||
lastLine := node.Lines().At(lastIndex)
|
||||
line := lastLine.Value(reader.Source())
|
||||
indicies := util.FindAttributeIndiciesReverse(line, true)
|
||||
if indicies != nil {
|
||||
for _, index := range indicies {
|
||||
node.SetAttribute(line[index[0]:index[1]], line[index[2]:index[3]])
|
||||
}
|
||||
lastLine.Stop = lastLine.Start + indicies[0][0] - 1
|
||||
lastLine.TrimRightSpace(reader.Source())
|
||||
node.Lines().Set(lastIndex, lastLine)
|
||||
tp := tmp.Parent()
|
||||
if tp != nil {
|
||||
tp.RemoveChild(tp, tmp)
|
||||
}
|
||||
}
|
||||
|
||||
if b.Attribute {
|
||||
parseLastLineAttributes(node, reader, pc)
|
||||
}
|
||||
|
||||
if b.AutoHeadingID {
|
||||
_, ok := node.AttributeString("id")
|
||||
id, ok := node.AttributeString("id")
|
||||
if !ok {
|
||||
generateAutoHeadingID(heading, reader, pc)
|
||||
} else {
|
||||
pc.IDs().Put(id.([]byte))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
type themanticBreakParser struct {
|
||||
}
|
||||
|
||||
var defaultThemanticBreakParser = &themanticBreakParser{}
|
||||
|
||||
// NewThemanticBreakParser returns a new BlockParser that
|
||||
// parses themantic breaks.
|
||||
func NewThemanticBreakParser() BlockParser {
|
||||
return defaultThemanticBreakParser
|
||||
}
|
||||
|
||||
func isThemanticBreak(line []byte) bool {
|
||||
w, pos := util.IndentWidth(line, 0)
|
||||
if w > 3 {
|
||||
return false
|
||||
}
|
||||
mark := byte(0)
|
||||
count := 0
|
||||
for i := pos; i < len(line); i++ {
|
||||
c := line[i]
|
||||
if util.IsSpace(c) {
|
||||
continue
|
||||
}
|
||||
if mark == 0 {
|
||||
mark = c
|
||||
count = 1
|
||||
if mark == '*' || mark == '-' || mark == '_' {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
if c != mark {
|
||||
return false
|
||||
}
|
||||
count++
|
||||
}
|
||||
return count > 2
|
||||
}
|
||||
|
||||
func (b *themanticBreakParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||
line, segment := reader.PeekLine()
|
||||
if isThemanticBreak(line) {
|
||||
reader.Advance(segment.Len() - 1)
|
||||
return ast.NewThemanticBreak(), NoChildren
|
||||
}
|
||||
return nil, NoChildren
|
||||
}
|
||||
|
||||
func (b *themanticBreakParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||
return Close
|
||||
}
|
||||
|
||||
func (b *themanticBreakParser) Close(node ast.Node, reader text.Reader, pc Context) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
func (b *themanticBreakParser) CanInterruptParagraph() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *themanticBreakParser) CanAcceptIndentedLine() bool {
|
||||
return false
|
||||
}
|
||||
75
parser/thematic_break.go
Normal file
75
parser/thematic_break.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/text"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
type thematicBreakPraser struct {
|
||||
}
|
||||
|
||||
var defaultThematicBreakPraser = &thematicBreakPraser{}
|
||||
|
||||
// NewThematicBreakParser returns a new BlockParser that
|
||||
// parses thematic breaks.
|
||||
func NewThematicBreakParser() BlockParser {
|
||||
return defaultThematicBreakPraser
|
||||
}
|
||||
|
||||
func isThematicBreak(line []byte, offset int) bool {
|
||||
w, pos := util.IndentWidth(line, offset)
|
||||
if w > 3 {
|
||||
return false
|
||||
}
|
||||
mark := byte(0)
|
||||
count := 0
|
||||
for i := pos; i < len(line); i++ {
|
||||
c := line[i]
|
||||
if util.IsSpace(c) {
|
||||
continue
|
||||
}
|
||||
if mark == 0 {
|
||||
mark = c
|
||||
count = 1
|
||||
if mark == '*' || mark == '-' || mark == '_' {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
if c != mark {
|
||||
return false
|
||||
}
|
||||
count++
|
||||
}
|
||||
return count > 2
|
||||
}
|
||||
|
||||
func (b *thematicBreakPraser) Trigger() []byte {
|
||||
return []byte{'-', '*', '_'}
|
||||
}
|
||||
|
||||
func (b *thematicBreakPraser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||||
line, segment := reader.PeekLine()
|
||||
if isThematicBreak(line, reader.LineOffset()) {
|
||||
reader.Advance(segment.Len() - 1)
|
||||
return ast.NewThematicBreak(), NoChildren
|
||||
}
|
||||
return nil, NoChildren
|
||||
}
|
||||
|
||||
func (b *thematicBreakPraser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||||
return Close
|
||||
}
|
||||
|
||||
func (b *thematicBreakPraser) Close(node ast.Node, reader text.Reader, pc Context) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
func (b *thematicBreakPraser) CanInterruptParagraph() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *thematicBreakPraser) CanAcceptIndentedLine() bool {
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
// Package html implements renderer that outputs HTMLs.
|
||||
package html
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
|
|
@ -12,19 +15,21 @@ import (
|
|||
|
||||
// A Config struct has configurations for the HTML based renderers.
|
||||
type Config struct {
|
||||
Writer Writer
|
||||
HardWraps bool
|
||||
XHTML bool
|
||||
Unsafe bool
|
||||
Writer Writer
|
||||
HardWraps bool
|
||||
EastAsianLineBreaks EastAsianLineBreaks
|
||||
XHTML bool
|
||||
Unsafe bool
|
||||
}
|
||||
|
||||
// NewConfig returns a new Config with defaults.
|
||||
func NewConfig() Config {
|
||||
return Config{
|
||||
Writer: DefaultWriter,
|
||||
HardWraps: false,
|
||||
XHTML: false,
|
||||
Unsafe: false,
|
||||
Writer: DefaultWriter,
|
||||
HardWraps: false,
|
||||
EastAsianLineBreaks: EastAsianLineBreaksNone,
|
||||
XHTML: false,
|
||||
Unsafe: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -33,6 +38,8 @@ func (c *Config) SetOption(name renderer.OptionName, value interface{}) {
|
|||
switch name {
|
||||
case optHardWraps:
|
||||
c.HardWraps = value.(bool)
|
||||
case optEastAsianLineBreaks:
|
||||
c.EastAsianLineBreaks = value.(EastAsianLineBreaks)
|
||||
case optXHTML:
|
||||
c.XHTML = value.(bool)
|
||||
case optUnsafe:
|
||||
|
|
@ -94,6 +101,99 @@ func WithHardWraps() interface {
|
|||
return &withHardWraps{}
|
||||
}
|
||||
|
||||
// EastAsianLineBreaks is an option name used in WithEastAsianLineBreaks.
|
||||
const optEastAsianLineBreaks renderer.OptionName = "EastAsianLineBreaks"
|
||||
|
||||
// A EastAsianLineBreaks is a style of east asian line breaks.
|
||||
type EastAsianLineBreaks int
|
||||
|
||||
const (
|
||||
//EastAsianLineBreaksNone renders line breaks as it is.
|
||||
EastAsianLineBreaksNone EastAsianLineBreaks = iota
|
||||
// EastAsianLineBreaksSimple follows east_asian_line_breaks in Pandoc.
|
||||
EastAsianLineBreaksSimple
|
||||
// EastAsianLineBreaksCSS3Draft follows CSS text level3 "Segment Break Transformation Rules" with some enhancements.
|
||||
EastAsianLineBreaksCSS3Draft
|
||||
)
|
||||
|
||||
func (b EastAsianLineBreaks) softLineBreak(thisLastRune rune, siblingFirstRune rune) bool {
|
||||
switch b {
|
||||
case EastAsianLineBreaksNone:
|
||||
return false
|
||||
case EastAsianLineBreaksSimple:
|
||||
return !(util.IsEastAsianWideRune(thisLastRune) && util.IsEastAsianWideRune(siblingFirstRune))
|
||||
case EastAsianLineBreaksCSS3Draft:
|
||||
return eastAsianLineBreaksCSS3DraftSoftLineBreak(thisLastRune, siblingFirstRune)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func eastAsianLineBreaksCSS3DraftSoftLineBreak(thisLastRune rune, siblingFirstRune rune) bool {
|
||||
// Implements CSS text level3 Segment Break Transformation Rules with some enhancements.
|
||||
// References:
|
||||
// - https://www.w3.org/TR/2020/WD-css-text-3-20200429/#line-break-transform
|
||||
// - https://github.com/w3c/csswg-drafts/issues/5086
|
||||
|
||||
// Rule1:
|
||||
// If the character immediately before or immediately after the segment break is
|
||||
// the zero-width space character (U+200B), then the break is removed, leaving behind the zero-width space.
|
||||
if thisLastRune == '\u200B' || siblingFirstRune == '\u200B' {
|
||||
return false
|
||||
}
|
||||
|
||||
// Rule2:
|
||||
// Otherwise, if the East Asian Width property of both the character before and after the segment break is
|
||||
// F, W, or H (not A), and neither side is Hangul, then the segment break is removed.
|
||||
thisLastRuneEastAsianWidth := util.EastAsianWidth(thisLastRune)
|
||||
siblingFirstRuneEastAsianWidth := util.EastAsianWidth(siblingFirstRune)
|
||||
if (thisLastRuneEastAsianWidth == "F" ||
|
||||
thisLastRuneEastAsianWidth == "W" ||
|
||||
thisLastRuneEastAsianWidth == "H") &&
|
||||
(siblingFirstRuneEastAsianWidth == "F" ||
|
||||
siblingFirstRuneEastAsianWidth == "W" ||
|
||||
siblingFirstRuneEastAsianWidth == "H") {
|
||||
return unicode.Is(unicode.Hangul, thisLastRune) || unicode.Is(unicode.Hangul, siblingFirstRune)
|
||||
}
|
||||
|
||||
// Rule3:
|
||||
// Otherwise, if either the character before or after the segment break belongs to
|
||||
// the space-discarding character set and it is a Unicode Punctuation (P*) or U+3000,
|
||||
// then the segment break is removed.
|
||||
if util.IsSpaceDiscardingUnicodeRune(thisLastRune) ||
|
||||
unicode.IsPunct(thisLastRune) ||
|
||||
thisLastRune == '\u3000' ||
|
||||
util.IsSpaceDiscardingUnicodeRune(siblingFirstRune) ||
|
||||
unicode.IsPunct(siblingFirstRune) ||
|
||||
siblingFirstRune == '\u3000' {
|
||||
return false
|
||||
}
|
||||
|
||||
// Rule4:
|
||||
// Otherwise, the segment break is converted to a space (U+0020).
|
||||
return true
|
||||
}
|
||||
|
||||
type withEastAsianLineBreaks struct {
|
||||
eastAsianLineBreaksStyle EastAsianLineBreaks
|
||||
}
|
||||
|
||||
func (o *withEastAsianLineBreaks) SetConfig(c *renderer.Config) {
|
||||
c.Options[optEastAsianLineBreaks] = o.eastAsianLineBreaksStyle
|
||||
}
|
||||
|
||||
func (o *withEastAsianLineBreaks) SetHTMLOption(c *Config) {
|
||||
c.EastAsianLineBreaks = o.eastAsianLineBreaksStyle
|
||||
}
|
||||
|
||||
// WithEastAsianLineBreaks is a functional option that indicates whether softline breaks
|
||||
// between east asian wide characters should be ignored.
|
||||
func WithEastAsianLineBreaks(e EastAsianLineBreaks) interface {
|
||||
renderer.Option
|
||||
Option
|
||||
} {
|
||||
return &withEastAsianLineBreaks{e}
|
||||
}
|
||||
|
||||
// XHTML is an option name used in WithXHTML.
|
||||
const optXHTML renderer.OptionName = "XHTML"
|
||||
|
||||
|
|
@ -172,7 +272,7 @@ func (r *Renderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
|||
reg.Register(ast.KindListItem, r.renderListItem)
|
||||
reg.Register(ast.KindParagraph, r.renderParagraph)
|
||||
reg.Register(ast.KindTextBlock, r.renderTextBlock)
|
||||
reg.Register(ast.KindThemanticBreak, r.renderThemanticBreak)
|
||||
reg.Register(ast.KindThematicBreak, r.renderThematicBreak)
|
||||
|
||||
// inlines
|
||||
|
||||
|
|
@ -183,6 +283,7 @@ func (r *Renderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
|||
reg.Register(ast.KindLink, r.renderLink)
|
||||
reg.Register(ast.KindRawHTML, r.renderRawHTML)
|
||||
reg.Register(ast.KindText, r.renderText)
|
||||
reg.Register(ast.KindString, r.renderString)
|
||||
}
|
||||
|
||||
func (r *Renderer) writeLines(w util.BufWriter, source []byte, n ast.Node) {
|
||||
|
|
@ -193,92 +294,147 @@ func (r *Renderer) writeLines(w util.BufWriter, source []byte, n ast.Node) {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
// GlobalAttributeFilter defines attribute names which any elements can have.
|
||||
var GlobalAttributeFilter = util.NewBytesFilter(
|
||||
[]byte("accesskey"),
|
||||
[]byte("autocapitalize"),
|
||||
[]byte("autofocus"),
|
||||
[]byte("class"),
|
||||
[]byte("contenteditable"),
|
||||
[]byte("dir"),
|
||||
[]byte("draggable"),
|
||||
[]byte("enterkeyhint"),
|
||||
[]byte("hidden"),
|
||||
[]byte("id"),
|
||||
[]byte("inert"),
|
||||
[]byte("inputmode"),
|
||||
[]byte("is"),
|
||||
[]byte("itemid"),
|
||||
[]byte("itemprop"),
|
||||
[]byte("itemref"),
|
||||
[]byte("itemscope"),
|
||||
[]byte("itemtype"),
|
||||
[]byte("lang"),
|
||||
[]byte("part"),
|
||||
[]byte("role"),
|
||||
[]byte("slot"),
|
||||
[]byte("spellcheck"),
|
||||
[]byte("style"),
|
||||
[]byte("tabindex"),
|
||||
[]byte("title"),
|
||||
[]byte("translate"),
|
||||
)
|
||||
|
||||
func (r *Renderer) renderDocument(
|
||||
w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
// nothing to do
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
var attrNameID = []byte("id")
|
||||
// HeadingAttributeFilter defines attribute names which heading elements can have.
|
||||
var HeadingAttributeFilter = GlobalAttributeFilter
|
||||
|
||||
func (r *Renderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
func (r *Renderer) renderHeading(
|
||||
w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.Heading)
|
||||
if entering {
|
||||
w.WriteString("<h")
|
||||
w.WriteByte("0123456"[n.Level])
|
||||
_, _ = w.WriteString("<h")
|
||||
_ = w.WriteByte("0123456"[n.Level])
|
||||
if n.Attributes() != nil {
|
||||
r.RenderAttributes(w, node)
|
||||
RenderAttributes(w, node, HeadingAttributeFilter)
|
||||
}
|
||||
w.WriteByte('>')
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
w.WriteString("</h")
|
||||
w.WriteByte("0123456"[n.Level])
|
||||
w.WriteString(">\n")
|
||||
_, _ = w.WriteString("</h")
|
||||
_ = w.WriteByte("0123456"[n.Level])
|
||||
_, _ = w.WriteString(">\n")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderBlockquote(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
// BlockquoteAttributeFilter defines attribute names which blockquote elements can have.
|
||||
var BlockquoteAttributeFilter = GlobalAttributeFilter.Extend(
|
||||
[]byte("cite"),
|
||||
)
|
||||
|
||||
func (r *Renderer) renderBlockquote(
|
||||
w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
w.WriteString("<blockquote>\n")
|
||||
if n.Attributes() != nil {
|
||||
_, _ = w.WriteString("<blockquote")
|
||||
RenderAttributes(w, n, BlockquoteAttributeFilter)
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
_, _ = w.WriteString("<blockquote>\n")
|
||||
}
|
||||
} else {
|
||||
w.WriteString("</blockquote>\n")
|
||||
_, _ = w.WriteString("</blockquote>\n")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderCodeBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
w.WriteString("<pre><code>")
|
||||
_, _ = w.WriteString("<pre><code>")
|
||||
r.writeLines(w, source, n)
|
||||
} else {
|
||||
w.WriteString("</code></pre>\n")
|
||||
_, _ = w.WriteString("</code></pre>\n")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
func (r *Renderer) renderFencedCodeBlock(
|
||||
w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.FencedCodeBlock)
|
||||
if entering {
|
||||
w.WriteString("<pre><code")
|
||||
_, _ = w.WriteString("<pre><code")
|
||||
language := n.Language(source)
|
||||
if language != nil {
|
||||
w.WriteString(" class=\"language-")
|
||||
_, _ = w.WriteString(" class=\"language-")
|
||||
r.Writer.Write(w, language)
|
||||
w.WriteString("\"")
|
||||
_, _ = w.WriteString("\"")
|
||||
}
|
||||
w.WriteByte('>')
|
||||
_ = w.WriteByte('>')
|
||||
r.writeLines(w, source, n)
|
||||
} else {
|
||||
w.WriteString("</code></pre>\n")
|
||||
_, _ = w.WriteString("</code></pre>\n")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
func (r *Renderer) renderHTMLBlock(
|
||||
w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.HTMLBlock)
|
||||
if entering {
|
||||
if r.Unsafe {
|
||||
l := n.Lines().Len()
|
||||
for i := 0; i < l; i++ {
|
||||
line := n.Lines().At(i)
|
||||
w.Write(line.Value(source))
|
||||
r.Writer.SecureWrite(w, line.Value(source))
|
||||
}
|
||||
} else {
|
||||
w.WriteString("<!-- raw HTML omitted -->\n")
|
||||
_, _ = w.WriteString("<!-- raw HTML omitted -->\n")
|
||||
}
|
||||
} else {
|
||||
if n.HasClosure() {
|
||||
if r.Unsafe {
|
||||
closure := n.ClosureLine
|
||||
w.Write(closure.Value(source))
|
||||
r.Writer.SecureWrite(w, closure.Value(source))
|
||||
} else {
|
||||
w.WriteString("<!-- raw HTML omitted -->\n")
|
||||
_, _ = w.WriteString("<!-- raw HTML omitted -->\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// ListAttributeFilter defines attribute names which list elements can have.
|
||||
var ListAttributeFilter = GlobalAttributeFilter.Extend(
|
||||
[]byte("start"),
|
||||
[]byte("reversed"),
|
||||
[]byte("type"),
|
||||
)
|
||||
|
||||
func (r *Renderer) renderList(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.List)
|
||||
tag := "ul"
|
||||
|
|
@ -286,119 +442,189 @@ func (r *Renderer) renderList(w util.BufWriter, source []byte, node ast.Node, en
|
|||
tag = "ol"
|
||||
}
|
||||
if entering {
|
||||
w.WriteByte('<')
|
||||
w.WriteString(tag)
|
||||
_ = w.WriteByte('<')
|
||||
_, _ = w.WriteString(tag)
|
||||
if n.IsOrdered() && n.Start != 1 {
|
||||
fmt.Fprintf(w, " start=\"%d\">\n", n.Start)
|
||||
} else {
|
||||
w.WriteString(">\n")
|
||||
_, _ = fmt.Fprintf(w, " start=\"%d\"", n.Start)
|
||||
}
|
||||
if n.Attributes() != nil {
|
||||
RenderAttributes(w, n, ListAttributeFilter)
|
||||
}
|
||||
_, _ = w.WriteString(">\n")
|
||||
} else {
|
||||
w.WriteString("</")
|
||||
w.WriteString(tag)
|
||||
w.WriteString(">\n")
|
||||
_, _ = w.WriteString("</")
|
||||
_, _ = w.WriteString(tag)
|
||||
_, _ = w.WriteString(">\n")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// ListItemAttributeFilter defines attribute names which list item elements can have.
|
||||
var ListItemAttributeFilter = GlobalAttributeFilter.Extend(
|
||||
[]byte("value"),
|
||||
)
|
||||
|
||||
func (r *Renderer) renderListItem(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
w.WriteString("<li>")
|
||||
if n.Attributes() != nil {
|
||||
_, _ = w.WriteString("<li")
|
||||
RenderAttributes(w, n, ListItemAttributeFilter)
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
_, _ = w.WriteString("<li>")
|
||||
}
|
||||
fc := n.FirstChild()
|
||||
if fc != nil {
|
||||
if _, ok := fc.(*ast.TextBlock); !ok {
|
||||
w.WriteByte('\n')
|
||||
_ = w.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
w.WriteString("</li>\n")
|
||||
_, _ = w.WriteString("</li>\n")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// ParagraphAttributeFilter defines attribute names which paragraph elements can have.
|
||||
var ParagraphAttributeFilter = GlobalAttributeFilter
|
||||
|
||||
func (r *Renderer) renderParagraph(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
w.WriteString("<p>")
|
||||
if n.Attributes() != nil {
|
||||
_, _ = w.WriteString("<p")
|
||||
RenderAttributes(w, n, ParagraphAttributeFilter)
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
_, _ = w.WriteString("<p>")
|
||||
}
|
||||
} else {
|
||||
w.WriteString("</p>\n")
|
||||
_, _ = w.WriteString("</p>\n")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderTextBlock(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
if _, ok := n.NextSibling().(ast.Node); ok && n.FirstChild() != nil {
|
||||
w.WriteByte('\n')
|
||||
if n.NextSibling() != nil && n.FirstChild() != nil {
|
||||
_ = w.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderThemanticBreak(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
// ThematicAttributeFilter defines attribute names which hr elements can have.
|
||||
var ThematicAttributeFilter = GlobalAttributeFilter.Extend(
|
||||
[]byte("align"), // [Deprecated]
|
||||
[]byte("color"), // [Not Standardized]
|
||||
[]byte("noshade"), // [Deprecated]
|
||||
[]byte("size"), // [Deprecated]
|
||||
[]byte("width"), // [Deprecated]
|
||||
)
|
||||
|
||||
func (r *Renderer) renderThematicBreak(
|
||||
w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
_, _ = w.WriteString("<hr")
|
||||
if n.Attributes() != nil {
|
||||
RenderAttributes(w, n, ThematicAttributeFilter)
|
||||
}
|
||||
if r.XHTML {
|
||||
w.WriteString("<hr />\n")
|
||||
_, _ = w.WriteString(" />\n")
|
||||
} else {
|
||||
w.WriteString("<hr>\n")
|
||||
_, _ = w.WriteString(">\n")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
// LinkAttributeFilter defines attribute names which link elements can have.
|
||||
var LinkAttributeFilter = GlobalAttributeFilter.Extend(
|
||||
[]byte("download"),
|
||||
// []byte("href"),
|
||||
[]byte("hreflang"),
|
||||
[]byte("media"),
|
||||
[]byte("ping"),
|
||||
[]byte("referrerpolicy"),
|
||||
[]byte("rel"),
|
||||
[]byte("shape"),
|
||||
[]byte("target"),
|
||||
)
|
||||
|
||||
func (r *Renderer) renderAutoLink(
|
||||
w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.AutoLink)
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
w.WriteString(`<a href="`)
|
||||
segment := n.Value.Segment
|
||||
value := segment.Value(source)
|
||||
if n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(value), []byte("mailto:")) {
|
||||
w.WriteString("mailto:")
|
||||
_, _ = w.WriteString(`<a href="`)
|
||||
url := n.URL(source)
|
||||
label := n.Label(source)
|
||||
if n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) {
|
||||
_, _ = w.WriteString("mailto:")
|
||||
}
|
||||
w.Write(util.EscapeHTML(util.URLEscape(value, false)))
|
||||
w.WriteString(`">`)
|
||||
w.Write(util.EscapeHTML(value))
|
||||
w.WriteString(`</a>`)
|
||||
_, _ = w.Write(util.EscapeHTML(util.URLEscape(url, false)))
|
||||
if n.Attributes() != nil {
|
||||
_ = w.WriteByte('"')
|
||||
RenderAttributes(w, n, LinkAttributeFilter)
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
_, _ = w.WriteString(`">`)
|
||||
}
|
||||
_, _ = w.Write(util.EscapeHTML(label))
|
||||
_, _ = w.WriteString(`</a>`)
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// CodeAttributeFilter defines attribute names which code elements can have.
|
||||
var CodeAttributeFilter = GlobalAttributeFilter
|
||||
|
||||
func (r *Renderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
w.WriteString("<code>")
|
||||
if n.Attributes() != nil {
|
||||
_, _ = w.WriteString("<code")
|
||||
RenderAttributes(w, n, CodeAttributeFilter)
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
_, _ = w.WriteString("<code>")
|
||||
}
|
||||
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
||||
segment := c.(*ast.Text).Segment
|
||||
value := segment.Value(source)
|
||||
if bytes.HasSuffix(value, []byte("\n")) {
|
||||
r.Writer.RawWrite(w, value[:len(value)-1])
|
||||
if c != n.LastChild() {
|
||||
r.Writer.RawWrite(w, []byte(" "))
|
||||
}
|
||||
r.Writer.RawWrite(w, []byte(" "))
|
||||
} else {
|
||||
r.Writer.RawWrite(w, value)
|
||||
}
|
||||
}
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
w.WriteString("</code>")
|
||||
_, _ = w.WriteString("</code>")
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderEmphasis(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
// EmphasisAttributeFilter defines attribute names which emphasis elements can have.
|
||||
var EmphasisAttributeFilter = GlobalAttributeFilter
|
||||
|
||||
func (r *Renderer) renderEmphasis(
|
||||
w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.Emphasis)
|
||||
tag := "em"
|
||||
if n.Level == 2 {
|
||||
tag = "strong"
|
||||
}
|
||||
if entering {
|
||||
w.WriteByte('<')
|
||||
w.WriteString(tag)
|
||||
w.WriteByte('>')
|
||||
_ = w.WriteByte('<')
|
||||
_, _ = w.WriteString(tag)
|
||||
if n.Attributes() != nil {
|
||||
RenderAttributes(w, n, EmphasisAttributeFilter)
|
||||
}
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
w.WriteString("</")
|
||||
w.WriteString(tag)
|
||||
w.WriteByte('>')
|
||||
_, _ = w.WriteString("</")
|
||||
_, _ = w.WriteString(tag)
|
||||
_ = w.WriteByte('>')
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
|
@ -406,48 +632,74 @@ func (r *Renderer) renderEmphasis(w util.BufWriter, source []byte, node ast.Node
|
|||
func (r *Renderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.Link)
|
||||
if entering {
|
||||
w.WriteString("<a href=\"")
|
||||
_, _ = w.WriteString("<a href=\"")
|
||||
if r.Unsafe || !IsDangerousURL(n.Destination) {
|
||||
w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
|
||||
_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
|
||||
}
|
||||
w.WriteByte('"')
|
||||
_ = w.WriteByte('"')
|
||||
if n.Title != nil {
|
||||
w.WriteString(` title="`)
|
||||
_, _ = w.WriteString(` title="`)
|
||||
r.Writer.Write(w, n.Title)
|
||||
w.WriteByte('"')
|
||||
_ = w.WriteByte('"')
|
||||
}
|
||||
w.WriteByte('>')
|
||||
if n.Attributes() != nil {
|
||||
RenderAttributes(w, n, LinkAttributeFilter)
|
||||
}
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
w.WriteString("</a>")
|
||||
_, _ = w.WriteString("</a>")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// ImageAttributeFilter defines attribute names which image elements can have.
|
||||
var ImageAttributeFilter = GlobalAttributeFilter.Extend(
|
||||
[]byte("align"),
|
||||
[]byte("border"),
|
||||
[]byte("crossorigin"),
|
||||
[]byte("decoding"),
|
||||
[]byte("height"),
|
||||
[]byte("importance"),
|
||||
[]byte("intrinsicsize"),
|
||||
[]byte("ismap"),
|
||||
[]byte("loading"),
|
||||
[]byte("referrerpolicy"),
|
||||
[]byte("sizes"),
|
||||
[]byte("srcset"),
|
||||
[]byte("usemap"),
|
||||
[]byte("width"),
|
||||
)
|
||||
|
||||
func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
n := node.(*ast.Image)
|
||||
w.WriteString("<img src=\"")
|
||||
_, _ = w.WriteString("<img src=\"")
|
||||
if r.Unsafe || !IsDangerousURL(n.Destination) {
|
||||
w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
|
||||
_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
|
||||
}
|
||||
w.WriteString(`" alt="`)
|
||||
w.Write(n.Text(source))
|
||||
w.WriteByte('"')
|
||||
_, _ = w.WriteString(`" alt="`)
|
||||
r.renderTexts(w, source, n)
|
||||
_ = w.WriteByte('"')
|
||||
if n.Title != nil {
|
||||
w.WriteString(` title="`)
|
||||
_, _ = w.WriteString(` title="`)
|
||||
r.Writer.Write(w, n.Title)
|
||||
w.WriteByte('"')
|
||||
_ = w.WriteByte('"')
|
||||
}
|
||||
if n.Attributes() != nil {
|
||||
RenderAttributes(w, n, ImageAttributeFilter)
|
||||
}
|
||||
if r.XHTML {
|
||||
w.WriteString(" />")
|
||||
_, _ = w.WriteString(" />")
|
||||
} else {
|
||||
w.WriteString(">")
|
||||
_, _ = w.WriteString(">")
|
||||
}
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
func (r *Renderer) renderRawHTML(
|
||||
w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
|
|
@ -456,11 +708,11 @@ func (r *Renderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node,
|
|||
l := n.Segments.Len()
|
||||
for i := 0; i < l; i++ {
|
||||
segment := n.Segments.At(i)
|
||||
w.Write(segment.Value(source))
|
||||
_, _ = w.Write(segment.Value(source))
|
||||
}
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
w.WriteString("<!-- raw HTML omitted -->")
|
||||
_, _ = w.WriteString("<!-- raw HTML omitted -->")
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
|
||||
|
|
@ -473,54 +725,162 @@ func (r *Renderer) renderText(w util.BufWriter, source []byte, node ast.Node, en
|
|||
if n.IsRaw() {
|
||||
r.Writer.RawWrite(w, segment.Value(source))
|
||||
} else {
|
||||
r.Writer.Write(w, segment.Value(source))
|
||||
value := segment.Value(source)
|
||||
r.Writer.Write(w, value)
|
||||
if n.HardLineBreak() || (n.SoftLineBreak() && r.HardWraps) {
|
||||
if r.XHTML {
|
||||
w.WriteString("<br />\n")
|
||||
_, _ = w.WriteString("<br />\n")
|
||||
} else {
|
||||
w.WriteString("<br>\n")
|
||||
_, _ = w.WriteString("<br>\n")
|
||||
}
|
||||
} else if n.SoftLineBreak() {
|
||||
w.WriteByte('\n')
|
||||
if r.EastAsianLineBreaks != EastAsianLineBreaksNone && len(value) != 0 {
|
||||
sibling := node.NextSibling()
|
||||
if sibling != nil && sibling.Kind() == ast.KindText {
|
||||
if siblingText := sibling.(*ast.Text).Value(source); len(siblingText) != 0 {
|
||||
thisLastRune := util.ToRune(value, len(value)-1)
|
||||
siblingFirstRune, _ := utf8.DecodeRune(siblingText)
|
||||
if r.EastAsianLineBreaks.softLineBreak(thisLastRune, siblingFirstRune) {
|
||||
_ = w.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_ = w.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
// RenderAttributes renders given node's attributes.
|
||||
func (r *Renderer) RenderAttributes(w util.BufWriter, node ast.Node) {
|
||||
for _, attr := range node.Attributes() {
|
||||
w.WriteString(" ")
|
||||
w.Write(attr.Name)
|
||||
w.WriteString(`="`)
|
||||
w.Write(attr.Value)
|
||||
w.WriteByte('"')
|
||||
func (r *Renderer) renderString(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if !entering {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
n := node.(*ast.String)
|
||||
if n.IsCode() {
|
||||
_, _ = w.Write(n.Value)
|
||||
} else {
|
||||
if n.IsRaw() {
|
||||
r.Writer.RawWrite(w, n.Value)
|
||||
} else {
|
||||
r.Writer.Write(w, n.Value)
|
||||
}
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *Renderer) renderTexts(w util.BufWriter, source []byte, n ast.Node) {
|
||||
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
||||
if s, ok := c.(*ast.String); ok {
|
||||
_, _ = r.renderString(w, source, s, true)
|
||||
} else if t, ok := c.(*ast.Text); ok {
|
||||
_, _ = r.renderText(w, source, t, true)
|
||||
} else {
|
||||
r.renderTexts(w, source, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A Writer interface wirtes textual contents to a writer.
|
||||
var dataPrefix = []byte("data-")
|
||||
|
||||
// RenderAttributes renders given node's attributes.
|
||||
// You can specify attribute names to render by the filter.
|
||||
// If filter is nil, RenderAttributes renders all attributes.
|
||||
func RenderAttributes(w util.BufWriter, node ast.Node, filter util.BytesFilter) {
|
||||
for _, attr := range node.Attributes() {
|
||||
if filter != nil && !filter.Contains(attr.Name) {
|
||||
if !bytes.HasPrefix(attr.Name, dataPrefix) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
_, _ = w.WriteString(" ")
|
||||
_, _ = w.Write(attr.Name)
|
||||
_, _ = w.WriteString(`="`)
|
||||
// TODO: convert numeric values to strings
|
||||
var value []byte
|
||||
switch typed := attr.Value.(type) {
|
||||
case []byte:
|
||||
value = typed
|
||||
case string:
|
||||
value = util.StringToReadOnlyBytes(typed)
|
||||
}
|
||||
_, _ = w.Write(util.EscapeHTML(value))
|
||||
_ = w.WriteByte('"')
|
||||
}
|
||||
}
|
||||
|
||||
// A Writer interface writes textual contents to a writer.
|
||||
type Writer interface {
|
||||
// Write writes the given source to writer with resolving references and unescaping
|
||||
// backslash escaped characters.
|
||||
Write(writer util.BufWriter, source []byte)
|
||||
|
||||
// RawWrite wirtes the given source to writer without resolving references and
|
||||
// RawWrite writes the given source to writer without resolving references and
|
||||
// unescaping backslash escaped characters.
|
||||
RawWrite(writer util.BufWriter, source []byte)
|
||||
|
||||
// SecureWrite writes the given source to writer with replacing insecure characters.
|
||||
SecureWrite(writer util.BufWriter, source []byte)
|
||||
}
|
||||
|
||||
var replacementCharacter = []byte("\ufffd")
|
||||
|
||||
// A WriterConfig struct has configurations for the HTML based writers.
|
||||
type WriterConfig struct {
|
||||
// EscapedSpace is an option that indicates that a '\' escaped half-space(0x20) should not be rendered.
|
||||
EscapedSpace bool
|
||||
}
|
||||
|
||||
// A WriterOption interface sets options for HTML based writers.
|
||||
type WriterOption func(*WriterConfig)
|
||||
|
||||
// WithEscapedSpace is a WriterOption indicates that a '\' escaped half-space(0x20) should not be rendered.
|
||||
func WithEscapedSpace() WriterOption {
|
||||
return func(c *WriterConfig) {
|
||||
c.EscapedSpace = true
|
||||
}
|
||||
}
|
||||
|
||||
type defaultWriter struct {
|
||||
WriterConfig
|
||||
}
|
||||
|
||||
// NewWriter returns a new Writer.
|
||||
func NewWriter(opts ...WriterOption) Writer {
|
||||
w := &defaultWriter{}
|
||||
for _, opt := range opts {
|
||||
opt(&w.WriterConfig)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
func escapeRune(writer util.BufWriter, r rune) {
|
||||
if r < 256 {
|
||||
v := util.EscapeHTMLByte(byte(r))
|
||||
if v != nil {
|
||||
writer.Write(v)
|
||||
_, _ = writer.Write(v)
|
||||
return
|
||||
}
|
||||
}
|
||||
writer.WriteRune(util.ToValidRune(r))
|
||||
_, _ = writer.WriteRune(util.ToValidRune(r))
|
||||
}
|
||||
|
||||
func (d *defaultWriter) SecureWrite(writer util.BufWriter, source []byte) {
|
||||
n := 0
|
||||
l := len(source)
|
||||
for i := 0; i < l; i++ {
|
||||
if source[i] == '\u0000' {
|
||||
_, _ = writer.Write(source[i-n : i])
|
||||
n = 0
|
||||
_, _ = writer.Write(replacementCharacter)
|
||||
continue
|
||||
}
|
||||
n++
|
||||
}
|
||||
if n != 0 {
|
||||
_, _ = writer.Write(source[l-n:])
|
||||
}
|
||||
}
|
||||
|
||||
func (d *defaultWriter) RawWrite(writer util.BufWriter, source []byte) {
|
||||
|
|
@ -529,21 +889,21 @@ func (d *defaultWriter) RawWrite(writer util.BufWriter, source []byte) {
|
|||
for i := 0; i < l; i++ {
|
||||
v := util.EscapeHTMLByte(source[i])
|
||||
if v != nil {
|
||||
writer.Write(source[i-n : i])
|
||||
_, _ = writer.Write(source[i-n : i])
|
||||
n = 0
|
||||
writer.Write(v)
|
||||
_, _ = writer.Write(v)
|
||||
continue
|
||||
}
|
||||
n++
|
||||
}
|
||||
if n != 0 {
|
||||
writer.Write(source[l-n:])
|
||||
_, _ = writer.Write(source[l-n:])
|
||||
}
|
||||
}
|
||||
|
||||
func (d *defaultWriter) Write(writer util.BufWriter, source []byte) {
|
||||
escaped := false
|
||||
ok := false
|
||||
var ok bool
|
||||
limit := len(source)
|
||||
n := 0
|
||||
for i := 0; i < limit; i++ {
|
||||
|
|
@ -555,34 +915,49 @@ func (d *defaultWriter) Write(writer util.BufWriter, source []byte) {
|
|||
escaped = false
|
||||
continue
|
||||
}
|
||||
if d.EscapedSpace && c == ' ' {
|
||||
d.RawWrite(writer, source[n:i-1])
|
||||
n = i + 1
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
if c == '\x00' {
|
||||
d.RawWrite(writer, source[n:i])
|
||||
d.RawWrite(writer, replacementCharacter)
|
||||
n = i + 1
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
if c == '&' {
|
||||
pos := i
|
||||
next := i + 1
|
||||
if next < limit && source[next] == '#' {
|
||||
nnext := next + 1
|
||||
nc := source[nnext]
|
||||
// code point like #x22;
|
||||
if nnext < limit && nc == 'x' || nc == 'X' {
|
||||
start := nnext + 1
|
||||
i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsHexDecimal)
|
||||
if ok && i < limit && source[i] == ';' {
|
||||
v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 16, 32)
|
||||
d.RawWrite(writer, source[n:pos])
|
||||
n = i + 1
|
||||
escapeRune(writer, rune(v))
|
||||
continue
|
||||
}
|
||||
// code point like #1234;
|
||||
} else if nc >= '0' && nc <= '9' {
|
||||
start := nnext
|
||||
i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsNumeric)
|
||||
if ok && i < limit && i-start < 8 && source[i] == ';' {
|
||||
v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 0, 32)
|
||||
d.RawWrite(writer, source[n:pos])
|
||||
n = i + 1
|
||||
escapeRune(writer, rune(v))
|
||||
continue
|
||||
if nnext < limit {
|
||||
nc := source[nnext]
|
||||
// code point like #x22;
|
||||
if nnext < limit && nc == 'x' || nc == 'X' {
|
||||
start := nnext + 1
|
||||
i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsHexDecimal)
|
||||
if ok && i < limit && source[i] == ';' && i-start < 7 {
|
||||
v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 16, 32)
|
||||
d.RawWrite(writer, source[n:pos])
|
||||
n = i + 1
|
||||
escapeRune(writer, rune(v))
|
||||
continue
|
||||
}
|
||||
// code point like #1234;
|
||||
} else if nc >= '0' && nc <= '9' {
|
||||
start := nnext
|
||||
i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsNumeric)
|
||||
if ok && i < limit && i-start < 8 && source[i] == ';' {
|
||||
v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 10, 32)
|
||||
d.RawWrite(writer, source[n:pos])
|
||||
n = i + 1
|
||||
escapeRune(writer, rune(v))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -608,33 +983,39 @@ func (d *defaultWriter) Write(writer util.BufWriter, source []byte) {
|
|||
}
|
||||
escaped = false
|
||||
}
|
||||
d.RawWrite(writer, source[n:len(source)])
|
||||
d.RawWrite(writer, source[n:])
|
||||
}
|
||||
|
||||
// DefaultWriter is a default implementation of the Writer.
|
||||
var DefaultWriter = &defaultWriter{}
|
||||
// DefaultWriter is a default instance of the Writer.
|
||||
var DefaultWriter = NewWriter()
|
||||
|
||||
var bDataImage = []byte("data:image/")
|
||||
var bPng = []byte("png;")
|
||||
var bGif = []byte("gif;")
|
||||
var bJpeg = []byte("jpeg;")
|
||||
var bWebp = []byte("webp;")
|
||||
var bSvg = []byte("svg+xml;")
|
||||
var bJs = []byte("javascript:")
|
||||
var bVb = []byte("vbscript:")
|
||||
var bFile = []byte("file:")
|
||||
var bData = []byte("data:")
|
||||
|
||||
func hasPrefix(s, prefix []byte) bool {
|
||||
return len(s) >= len(prefix) && bytes.Equal(bytes.ToLower(s[0:len(prefix)]), bytes.ToLower(prefix))
|
||||
}
|
||||
|
||||
// IsDangerousURL returns true if the given url seems a potentially dangerous url,
|
||||
// otherwise false.
|
||||
func IsDangerousURL(url []byte) bool {
|
||||
if bytes.HasPrefix(url, bDataImage) && len(url) >= 11 {
|
||||
if hasPrefix(url, bDataImage) && len(url) >= 11 {
|
||||
v := url[11:]
|
||||
if bytes.HasPrefix(v, bPng) || bytes.HasPrefix(v, bGif) ||
|
||||
bytes.HasPrefix(v, bJpeg) || bytes.HasPrefix(v, bWebp) {
|
||||
if hasPrefix(v, bPng) || hasPrefix(v, bGif) ||
|
||||
hasPrefix(v, bJpeg) || hasPrefix(v, bWebp) ||
|
||||
hasPrefix(v, bSvg) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return bytes.HasPrefix(url, bJs) || bytes.HasPrefix(url, bVb) ||
|
||||
bytes.HasPrefix(url, bFile) || bytes.HasPrefix(url, bData)
|
||||
return hasPrefix(url, bJs) || hasPrefix(url, bVb) ||
|
||||
hasPrefix(url, bFile) || hasPrefix(url, bData)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
// Package renderer renders the given AST to certain formats.
|
||||
// Package renderer renders the given AST to certain formats.
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/util"
|
||||
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A Config struct is a data structure that holds configuration of the Renderer.
|
||||
|
|
@ -17,7 +16,7 @@ type Config struct {
|
|||
NodeRenderers util.PrioritizedSlice
|
||||
}
|
||||
|
||||
// NewConfig returns a new Config
|
||||
// NewConfig returns a new Config.
|
||||
func NewConfig() *Config {
|
||||
return &Config{
|
||||
Options: map[OptionName]interface{}{},
|
||||
|
|
@ -57,7 +56,7 @@ func (o *withOption) SetConfig(c *Config) {
|
|||
}
|
||||
|
||||
// WithOption is a functional option that allow you to set
|
||||
// an arbitary option to the parser.
|
||||
// an arbitrary option to the parser.
|
||||
func WithOption(name OptionName, value interface{}) Option {
|
||||
return &withOption{name, value}
|
||||
}
|
||||
|
|
@ -79,7 +78,7 @@ type NodeRenderer interface {
|
|||
RegisterFuncs(NodeRendererFuncRegisterer)
|
||||
}
|
||||
|
||||
// A NodeRendererFuncRegisterer registers
|
||||
// A NodeRendererFuncRegisterer registers given NodeRendererFunc to this object.
|
||||
type NodeRendererFuncRegisterer interface {
|
||||
// Register registers given NodeRendererFunc to this object.
|
||||
Register(ast.NodeKind, NodeRendererFunc)
|
||||
|
|
@ -90,7 +89,7 @@ type NodeRendererFuncRegisterer interface {
|
|||
type Renderer interface {
|
||||
Render(w io.Writer, source []byte, n ast.Node) error
|
||||
|
||||
// AddOptions adds given option to thie parser.
|
||||
// AddOptions adds given option to this renderer.
|
||||
AddOptions(...Option)
|
||||
}
|
||||
|
||||
|
|
|
|||
409
testutil/testutil.go
Normal file
409
testutil/testutil.go
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
// Package testutil provides utilities for unit tests.
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
// TestingT is a subset of the functionality provided by testing.T.
|
||||
type TestingT interface {
|
||||
Logf(string, ...interface{})
|
||||
Skipf(string, ...interface{})
|
||||
Errorf(string, ...interface{})
|
||||
FailNow()
|
||||
}
|
||||
|
||||
// MarkdownTestCase represents a test case.
|
||||
type MarkdownTestCase struct {
|
||||
No int
|
||||
Description string
|
||||
Options MarkdownTestCaseOptions
|
||||
Markdown string
|
||||
Expected string
|
||||
}
|
||||
|
||||
func source(t *MarkdownTestCase) string {
|
||||
ret := t.Markdown
|
||||
if t.Options.Trim {
|
||||
ret = strings.TrimSpace(ret)
|
||||
}
|
||||
if t.Options.EnableEscape {
|
||||
return string(applyEscapeSequence([]byte(ret)))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func expected(t *MarkdownTestCase) string {
|
||||
ret := t.Expected
|
||||
if t.Options.Trim {
|
||||
ret = strings.TrimSpace(ret)
|
||||
}
|
||||
if t.Options.EnableEscape {
|
||||
return string(applyEscapeSequence([]byte(ret)))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// MarkdownTestCaseOptions represents options for each test case.
|
||||
type MarkdownTestCaseOptions struct {
|
||||
EnableEscape bool
|
||||
Trim bool
|
||||
}
|
||||
|
||||
const attributeSeparator = "//- - - - - - - - -//"
|
||||
const caseSeparator = "//= = = = = = = = = = = = = = = = = = = = = = = =//"
|
||||
|
||||
var optionsRegexp = regexp.MustCompile(`(?i)\s*options:(.*)`)
|
||||
|
||||
// ParseCliCaseArg parses -case command line args.
|
||||
func ParseCliCaseArg() []int {
|
||||
ret := []int{}
|
||||
for _, a := range os.Args {
|
||||
if strings.HasPrefix(a, "case=") {
|
||||
parts := strings.Split(a, "=")
|
||||
for _, cas := range strings.Split(parts[1], ",") {
|
||||
value, err := strconv.Atoi(strings.TrimSpace(cas))
|
||||
if err == nil {
|
||||
ret = append(ret, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// DoTestCaseFile runs test cases in a given file.
|
||||
func DoTestCaseFile(m goldmark.Markdown, filename string, t TestingT, no ...int) {
|
||||
fp, err := os.Open(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
_ = fp.Close()
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(fp)
|
||||
c := MarkdownTestCase{
|
||||
No: -1,
|
||||
Description: "",
|
||||
Options: MarkdownTestCaseOptions{},
|
||||
Markdown: "",
|
||||
Expected: "",
|
||||
}
|
||||
cases := []MarkdownTestCase{}
|
||||
line := 0
|
||||
for scanner.Scan() {
|
||||
line++
|
||||
if util.IsBlank([]byte(scanner.Text())) {
|
||||
continue
|
||||
}
|
||||
header := scanner.Text()
|
||||
c.Description = ""
|
||||
if strings.Contains(header, ":") {
|
||||
parts := strings.Split(header, ":")
|
||||
c.No, err = strconv.Atoi(strings.TrimSpace(parts[0]))
|
||||
c.Description = strings.Join(parts[1:], ":")
|
||||
} else {
|
||||
c.No, err = strconv.Atoi(scanner.Text())
|
||||
}
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("%s: invalid case No at line %d", filename, line))
|
||||
}
|
||||
if !scanner.Scan() {
|
||||
panic(fmt.Sprintf("%s: invalid case at line %d", filename, line))
|
||||
}
|
||||
line++
|
||||
matches := optionsRegexp.FindAllStringSubmatch(scanner.Text(), -1)
|
||||
if len(matches) != 0 {
|
||||
err = json.Unmarshal([]byte(matches[0][1]), &c.Options)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("%s: invalid options at line %d", filename, line))
|
||||
}
|
||||
scanner.Scan()
|
||||
line++
|
||||
}
|
||||
if scanner.Text() != attributeSeparator {
|
||||
panic(fmt.Sprintf("%s: invalid separator '%s' at line %d", filename, scanner.Text(), line))
|
||||
}
|
||||
buf := []string{}
|
||||
for scanner.Scan() {
|
||||
line++
|
||||
text := scanner.Text()
|
||||
if text == attributeSeparator {
|
||||
break
|
||||
}
|
||||
buf = append(buf, text)
|
||||
}
|
||||
c.Markdown = strings.Join(buf, "\n")
|
||||
buf = []string{}
|
||||
for scanner.Scan() {
|
||||
line++
|
||||
text := scanner.Text()
|
||||
if text == caseSeparator {
|
||||
break
|
||||
}
|
||||
buf = append(buf, text)
|
||||
}
|
||||
c.Expected = strings.Join(buf, "\n")
|
||||
if len(c.Expected) != 0 {
|
||||
c.Expected = c.Expected + "\n"
|
||||
}
|
||||
shouldAdd := len(no) == 0
|
||||
if !shouldAdd {
|
||||
for _, n := range no {
|
||||
if n == c.No {
|
||||
shouldAdd = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if shouldAdd {
|
||||
cases = append(cases, c)
|
||||
}
|
||||
}
|
||||
DoTestCases(m, cases, t)
|
||||
}
|
||||
|
||||
// DoTestCases runs a set of test cases.
|
||||
func DoTestCases(m goldmark.Markdown, cases []MarkdownTestCase, t TestingT, opts ...parser.ParseOption) {
|
||||
for _, testCase := range cases {
|
||||
DoTestCase(m, testCase, t, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// DoTestCase runs a test case.
|
||||
func DoTestCase(m goldmark.Markdown, testCase MarkdownTestCase, t TestingT, opts ...parser.ParseOption) {
|
||||
var ok bool
|
||||
var out bytes.Buffer
|
||||
defer func() {
|
||||
description := ""
|
||||
if len(testCase.Description) != 0 {
|
||||
description = ": " + testCase.Description
|
||||
}
|
||||
if err := recover(); err != nil {
|
||||
format := `============= case %d%s ================
|
||||
Markdown:
|
||||
-----------
|
||||
%s
|
||||
|
||||
Expected:
|
||||
----------
|
||||
%s
|
||||
|
||||
Actual
|
||||
---------
|
||||
%v
|
||||
%s
|
||||
`
|
||||
t.Errorf(format, testCase.No, description, source(&testCase), expected(&testCase), err, debug.Stack())
|
||||
} else if !ok {
|
||||
format := `============= case %d%s ================
|
||||
Markdown:
|
||||
-----------
|
||||
%s
|
||||
|
||||
Expected:
|
||||
----------
|
||||
%s
|
||||
|
||||
Actual
|
||||
---------
|
||||
%s
|
||||
|
||||
Diff
|
||||
---------
|
||||
%s
|
||||
`
|
||||
t.Errorf(format, testCase.No, description, source(&testCase), expected(&testCase), out.Bytes(),
|
||||
DiffPretty([]byte(expected(&testCase)), out.Bytes()))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := m.Convert([]byte(source(&testCase)), &out, opts...); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ok = bytes.Equal(bytes.TrimSpace(out.Bytes()), bytes.TrimSpace([]byte(expected(&testCase))))
|
||||
}
|
||||
|
||||
type diffType int
|
||||
|
||||
const (
|
||||
diffRemoved diffType = iota
|
||||
diffAdded
|
||||
diffNone
|
||||
)
|
||||
|
||||
type diff struct {
|
||||
Type diffType
|
||||
Lines [][]byte
|
||||
}
|
||||
|
||||
func simpleDiff(v1, v2 []byte) []diff {
|
||||
return simpleDiffAux(
|
||||
bytes.Split(v1, []byte("\n")),
|
||||
bytes.Split(v2, []byte("\n")))
|
||||
}
|
||||
|
||||
func simpleDiffAux(v1lines, v2lines [][]byte) []diff {
|
||||
v1index := map[string][]int{}
|
||||
for i, line := range v1lines {
|
||||
key := util.BytesToReadOnlyString(line)
|
||||
if _, ok := v1index[key]; !ok {
|
||||
v1index[key] = []int{}
|
||||
}
|
||||
v1index[key] = append(v1index[key], i)
|
||||
}
|
||||
overlap := map[int]int{}
|
||||
v1start := 0
|
||||
v2start := 0
|
||||
length := 0
|
||||
for v2pos, line := range v2lines {
|
||||
newOverlap := map[int]int{}
|
||||
key := util.BytesToReadOnlyString(line)
|
||||
if _, ok := v1index[key]; !ok {
|
||||
v1index[key] = []int{}
|
||||
}
|
||||
for _, v1pos := range v1index[key] {
|
||||
value := 0
|
||||
if v1pos != 0 {
|
||||
if v, ok := overlap[v1pos-1]; ok {
|
||||
value = v
|
||||
}
|
||||
}
|
||||
newOverlap[v1pos] = value + 1
|
||||
if newOverlap[v1pos] > length {
|
||||
length = newOverlap[v1pos]
|
||||
v1start = v1pos - length + 1
|
||||
v2start = v2pos - length + 1
|
||||
}
|
||||
}
|
||||
overlap = newOverlap
|
||||
}
|
||||
if length == 0 {
|
||||
diffs := []diff{}
|
||||
if len(v1lines) != 0 {
|
||||
diffs = append(diffs, diff{diffRemoved, v1lines})
|
||||
}
|
||||
if len(v2lines) != 0 {
|
||||
diffs = append(diffs, diff{diffAdded, v2lines})
|
||||
}
|
||||
return diffs
|
||||
}
|
||||
diffs := simpleDiffAux(v1lines[:v1start], v2lines[:v2start])
|
||||
diffs = append(diffs, diff{diffNone, v2lines[v2start : v2start+length]})
|
||||
diffs = append(diffs, simpleDiffAux(v1lines[v1start+length:],
|
||||
v2lines[v2start+length:])...)
|
||||
return diffs
|
||||
}
|
||||
|
||||
// DiffPretty returns pretty formatted diff between given bytes.
|
||||
func DiffPretty(v1, v2 []byte) []byte {
|
||||
var b bytes.Buffer
|
||||
diffs := simpleDiff(v1, v2)
|
||||
for _, diff := range diffs {
|
||||
c := " "
|
||||
switch diff.Type {
|
||||
case diffAdded:
|
||||
c = "+"
|
||||
case diffRemoved:
|
||||
c = "-"
|
||||
case diffNone:
|
||||
c = " "
|
||||
}
|
||||
for _, line := range diff.Lines {
|
||||
if c != " " {
|
||||
b.WriteString(fmt.Sprintf("%s | %s\n", c, util.VisualizeSpaces(line)))
|
||||
} else {
|
||||
b.WriteString(fmt.Sprintf("%s | %s\n", c, line))
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func applyEscapeSequence(b []byte) []byte {
|
||||
result := make([]byte, 0, len(b))
|
||||
for i := 0; i < len(b); i++ {
|
||||
if b[i] == '\\' && i != len(b)-1 {
|
||||
switch b[i+1] {
|
||||
case 'a':
|
||||
result = append(result, '\a')
|
||||
i++
|
||||
continue
|
||||
case 'b':
|
||||
result = append(result, '\b')
|
||||
i++
|
||||
continue
|
||||
case 'f':
|
||||
result = append(result, '\f')
|
||||
i++
|
||||
continue
|
||||
case 'n':
|
||||
result = append(result, '\n')
|
||||
i++
|
||||
continue
|
||||
case 'r':
|
||||
result = append(result, '\r')
|
||||
i++
|
||||
continue
|
||||
case 't':
|
||||
result = append(result, '\t')
|
||||
i++
|
||||
continue
|
||||
case 'v':
|
||||
result = append(result, '\v')
|
||||
i++
|
||||
continue
|
||||
case '\\':
|
||||
result = append(result, '\\')
|
||||
i++
|
||||
continue
|
||||
case 'x':
|
||||
if len(b) >= i+3 && util.IsHexDecimal(b[i+2]) && util.IsHexDecimal(b[i+3]) {
|
||||
v, _ := hex.DecodeString(string(b[i+2 : i+4]))
|
||||
result = append(result, v[0])
|
||||
i += 3
|
||||
continue
|
||||
}
|
||||
case 'u', 'U':
|
||||
if len(b) > i+2 {
|
||||
num := []byte{}
|
||||
for j := i + 2; j < len(b); j++ {
|
||||
if util.IsHexDecimal(b[j]) {
|
||||
num = append(num, b[j])
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if len(num) >= 4 && len(num) < 8 {
|
||||
v, _ := strconv.ParseInt(string(num[:4]), 16, 32)
|
||||
result = append(result, []byte(string(rune(v)))...)
|
||||
i += 5
|
||||
continue
|
||||
}
|
||||
if len(num) >= 8 {
|
||||
v, _ := strconv.ParseInt(string(num[:8]), 16, 32)
|
||||
result = append(result, []byte(string(rune(v)))...)
|
||||
i += 9
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result = append(result, b[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
7
testutil/testutil_test.go
Normal file
7
testutil/testutil_test.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package testutil
|
||||
|
||||
import "testing"
|
||||
|
||||
// This will fail to compile if the TestingT interface is changed in a way
|
||||
// that doesn't conform to testing.T.
|
||||
var _ TestingT = (*testing.T)(nil)
|
||||
2
text/package.go
Normal file
2
text/package.go
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Package text provides functionalities to manipulate texts.
|
||||
package text
|
||||
199
text/reader.go
199
text/reader.go
|
|
@ -1,10 +1,12 @@
|
|||
package text
|
||||
|
||||
import (
|
||||
"github.com/yuin/goldmark/util"
|
||||
"bytes"
|
||||
"io"
|
||||
"regexp"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
const invalidValue = -1
|
||||
|
|
@ -69,6 +71,28 @@ type Reader interface {
|
|||
|
||||
// Match performs regular expression searching to current line.
|
||||
FindSubMatch(reg *regexp.Regexp) [][]byte
|
||||
|
||||
// FindClosure finds corresponding closure.
|
||||
FindClosure(opener, closer byte, options FindClosureOptions) (*Segments, bool)
|
||||
}
|
||||
|
||||
// FindClosureOptions is options for Reader.FindClosure.
|
||||
type FindClosureOptions struct {
|
||||
// CodeSpan is a flag for the FindClosure. If this is set to true,
|
||||
// FindClosure ignores closers in codespans.
|
||||
CodeSpan bool
|
||||
|
||||
// Nesting is a flag for the FindClosure. If this is set to true,
|
||||
// FindClosure allows nesting.
|
||||
Nesting bool
|
||||
|
||||
// Newline is a flag for the FindClosure. If this is set to true,
|
||||
// FindClosure searches for a closer over multiple lines.
|
||||
Newline bool
|
||||
|
||||
// Advance is a flag for the FindClosure. If this is set to true,
|
||||
// FindClosure advances pointers when closer is found.
|
||||
Advance bool
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
|
|
@ -78,6 +102,7 @@ type reader struct {
|
|||
peekedLine []byte
|
||||
pos Segment
|
||||
head int
|
||||
lineOffset int
|
||||
}
|
||||
|
||||
// NewReader return a new Reader that can read UTF-8 bytes .
|
||||
|
|
@ -90,9 +115,14 @@ func NewReader(source []byte) Reader {
|
|||
return r
|
||||
}
|
||||
|
||||
func (r *reader) FindClosure(opener, closer byte, options FindClosureOptions) (*Segments, bool) {
|
||||
return findClosureReader(r, opener, closer, options)
|
||||
}
|
||||
|
||||
func (r *reader) ResetPosition() {
|
||||
r.line = -1
|
||||
r.head = 0
|
||||
r.lineOffset = -1
|
||||
r.AdvanceLine()
|
||||
}
|
||||
|
||||
|
|
@ -124,17 +154,24 @@ func (r *reader) PeekLine() ([]byte, Segment) {
|
|||
return nil, r.pos
|
||||
}
|
||||
|
||||
// io.RuneReader interface
|
||||
// io.RuneReader interface.
|
||||
func (r *reader) ReadRune() (rune, int, error) {
|
||||
return readRuneReader(r)
|
||||
}
|
||||
|
||||
func (r *reader) LineOffset() int {
|
||||
v := r.pos.Start - r.head
|
||||
if r.pos.Padding > 0 {
|
||||
v += util.TabWidth(v) - r.pos.Padding
|
||||
if r.lineOffset < 0 {
|
||||
v := 0
|
||||
for i := r.head; i < r.pos.Start; i++ {
|
||||
if r.source[i] == '\t' {
|
||||
v += util.TabWidth(v)
|
||||
} else {
|
||||
v++
|
||||
}
|
||||
}
|
||||
r.lineOffset = v - r.pos.Padding
|
||||
}
|
||||
return v
|
||||
return r.lineOffset
|
||||
}
|
||||
|
||||
func (r *reader) PrecendingCharacter() rune {
|
||||
|
|
@ -155,6 +192,7 @@ func (r *reader) PrecendingCharacter() rune {
|
|||
}
|
||||
|
||||
func (r *reader) Advance(n int) {
|
||||
r.lineOffset = -1
|
||||
if n < len(r.peekedLine) && r.pos.Padding == 0 {
|
||||
r.pos.Start += n
|
||||
r.peekedLine = nil
|
||||
|
|
@ -183,6 +221,7 @@ func (r *reader) AdvanceAndSetPadding(n, padding int) {
|
|||
}
|
||||
|
||||
func (r *reader) AdvanceLine() {
|
||||
r.lineOffset = -1
|
||||
r.peekedLine = nil
|
||||
r.pos.Start = r.pos.Stop
|
||||
r.head = r.pos.Start
|
||||
|
|
@ -206,6 +245,7 @@ func (r *reader) Position() (int, Segment) {
|
|||
}
|
||||
|
||||
func (r *reader) SetPosition(line int, pos Segment) {
|
||||
r.lineOffset = -1
|
||||
r.line = line
|
||||
r.pos = pos
|
||||
}
|
||||
|
|
@ -245,6 +285,7 @@ type blockReader struct {
|
|||
pos Segment
|
||||
head int
|
||||
last int
|
||||
lineOffset int
|
||||
}
|
||||
|
||||
// NewBlockReader returns a new BlockReader.
|
||||
|
|
@ -258,10 +299,15 @@ func NewBlockReader(source []byte, segments *Segments) BlockReader {
|
|||
return r
|
||||
}
|
||||
|
||||
func (r *blockReader) FindClosure(opener, closer byte, options FindClosureOptions) (*Segments, bool) {
|
||||
return findClosureReader(r, opener, closer, options)
|
||||
}
|
||||
|
||||
func (r *blockReader) ResetPosition() {
|
||||
r.line = -1
|
||||
r.head = 0
|
||||
r.last = 0
|
||||
r.lineOffset = -1
|
||||
r.pos.Start = -1
|
||||
r.pos.Stop = -1
|
||||
r.pos.Padding = 0
|
||||
|
|
@ -308,7 +354,7 @@ func (r *blockReader) Value(seg Segment) []byte {
|
|||
return ret
|
||||
}
|
||||
|
||||
// io.RuneReader interface
|
||||
// io.RuneReader interface.
|
||||
func (r *blockReader) ReadRune() (rune, int, error) {
|
||||
return readRuneReader(r)
|
||||
}
|
||||
|
|
@ -317,25 +363,40 @@ func (r *blockReader) PrecendingCharacter() rune {
|
|||
if r.pos.Padding != 0 {
|
||||
return rune(' ')
|
||||
}
|
||||
if r.pos.Start <= 0 {
|
||||
if r.segments.Len() < 1 {
|
||||
return rune('\n')
|
||||
}
|
||||
firstSegment := r.segments.At(0)
|
||||
if r.line == 0 && r.pos.Start <= firstSegment.Start {
|
||||
return rune('\n')
|
||||
}
|
||||
l := len(r.source)
|
||||
i := r.pos.Start - 1
|
||||
for ; i >= 0; i-- {
|
||||
for ; i < l && i >= 0; i-- {
|
||||
if utf8.RuneStart(r.source[i]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i < 0 || i >= l {
|
||||
return rune('\n')
|
||||
}
|
||||
rn, _ := utf8.DecodeRune(r.source[i:])
|
||||
return rn
|
||||
}
|
||||
|
||||
func (r *blockReader) LineOffset() int {
|
||||
v := r.pos.Start - r.head
|
||||
if r.pos.Padding > 0 {
|
||||
v += util.TabWidth(v) - r.pos.Padding
|
||||
if r.lineOffset < 0 {
|
||||
v := 0
|
||||
for i := r.head; i < r.pos.Start; i++ {
|
||||
if r.source[i] == '\t' {
|
||||
v += util.TabWidth(v)
|
||||
} else {
|
||||
v++
|
||||
}
|
||||
}
|
||||
r.lineOffset = v - r.pos.Padding
|
||||
}
|
||||
return v
|
||||
return r.lineOffset
|
||||
}
|
||||
|
||||
func (r *blockReader) Peek() byte {
|
||||
|
|
@ -356,6 +417,8 @@ func (r *blockReader) PeekLine() ([]byte, Segment) {
|
|||
}
|
||||
|
||||
func (r *blockReader) Advance(n int) {
|
||||
r.lineOffset = -1
|
||||
|
||||
if n < r.pos.Stop-r.pos.Start && r.pos.Padding == 0 {
|
||||
r.pos.Start += n
|
||||
return
|
||||
|
|
@ -391,17 +454,25 @@ func (r *blockReader) Position() (int, Segment) {
|
|||
}
|
||||
|
||||
func (r *blockReader) SetPosition(line int, pos Segment) {
|
||||
r.lineOffset = -1
|
||||
r.line = line
|
||||
if pos.Start == invalidValue {
|
||||
if r.line < r.segmentsLength {
|
||||
r.pos = r.segments.At(line)
|
||||
s := r.segments.At(line)
|
||||
r.head = s.Start
|
||||
r.pos = s
|
||||
}
|
||||
} else {
|
||||
r.pos = pos
|
||||
if r.line < r.segmentsLength {
|
||||
s := r.segments.At(line)
|
||||
r.head = s.Start
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *blockReader) SetPadding(v int) {
|
||||
r.lineOffset = -1
|
||||
r.pos.Padding = v
|
||||
}
|
||||
|
||||
|
|
@ -467,24 +538,30 @@ func matchReader(r Reader, reg *regexp.Regexp) bool {
|
|||
}
|
||||
|
||||
func findSubMatchReader(r Reader, reg *regexp.Regexp) [][]byte {
|
||||
oldline, oldseg := r.Position()
|
||||
oldLine, oldSeg := r.Position()
|
||||
match := reg.FindReaderSubmatchIndex(r)
|
||||
r.SetPosition(oldline, oldseg)
|
||||
r.SetPosition(oldLine, oldSeg)
|
||||
if match == nil {
|
||||
return nil
|
||||
}
|
||||
runes := make([]rune, 0, match[1]-match[0])
|
||||
var bb bytes.Buffer
|
||||
bb.Grow(match[1] - match[0])
|
||||
for i := 0; i < match[1]; {
|
||||
r, size, _ := readRuneReader(r)
|
||||
i += size
|
||||
runes = append(runes, r)
|
||||
bb.WriteRune(r)
|
||||
}
|
||||
result := [][]byte{}
|
||||
bs := bb.Bytes()
|
||||
var result [][]byte
|
||||
for i := 0; i < len(match); i += 2 {
|
||||
result = append(result, []byte(string(runes[match[i]:match[i+1]])))
|
||||
if match[i] < 0 {
|
||||
result = append(result, []byte{})
|
||||
continue
|
||||
}
|
||||
result = append(result, bs[match[i]:match[i+1]])
|
||||
}
|
||||
|
||||
r.SetPosition(oldline, oldseg)
|
||||
r.SetPosition(oldLine, oldSeg)
|
||||
r.Advance(match[1] - match[0])
|
||||
return result
|
||||
}
|
||||
|
|
@ -501,3 +578,83 @@ func readRuneReader(r Reader) (rune, int, error) {
|
|||
r.Advance(size)
|
||||
return rn, size, nil
|
||||
}
|
||||
|
||||
func findClosureReader(r Reader, opener, closer byte, opts FindClosureOptions) (*Segments, bool) {
|
||||
opened := 1
|
||||
codeSpanOpener := 0
|
||||
closed := false
|
||||
orgline, orgpos := r.Position()
|
||||
var ret *Segments
|
||||
|
||||
for {
|
||||
bs, seg := r.PeekLine()
|
||||
if bs == nil {
|
||||
goto end
|
||||
}
|
||||
i := 0
|
||||
for i < len(bs) {
|
||||
c := bs[i]
|
||||
if opts.CodeSpan && codeSpanOpener != 0 && c == '`' {
|
||||
codeSpanCloser := 0
|
||||
for ; i < len(bs); i++ {
|
||||
if bs[i] == '`' {
|
||||
codeSpanCloser++
|
||||
} else {
|
||||
i--
|
||||
break
|
||||
}
|
||||
}
|
||||
if codeSpanCloser == codeSpanOpener {
|
||||
codeSpanOpener = 0
|
||||
}
|
||||
} else if codeSpanOpener == 0 && c == '\\' && i < len(bs)-1 && util.IsPunct(bs[i+1]) {
|
||||
i += 2
|
||||
continue
|
||||
} else if opts.CodeSpan && codeSpanOpener == 0 && c == '`' {
|
||||
for ; i < len(bs); i++ {
|
||||
if bs[i] == '`' {
|
||||
codeSpanOpener++
|
||||
} else {
|
||||
i--
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (opts.CodeSpan && codeSpanOpener == 0) || !opts.CodeSpan {
|
||||
if c == closer {
|
||||
opened--
|
||||
if opened == 0 {
|
||||
if ret == nil {
|
||||
ret = NewSegments()
|
||||
}
|
||||
ret.Append(seg.WithStop(seg.Start + i))
|
||||
r.Advance(i + 1)
|
||||
closed = true
|
||||
goto end
|
||||
}
|
||||
} else if c == opener {
|
||||
if !opts.Nesting {
|
||||
goto end
|
||||
}
|
||||
opened++
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
if !opts.Newline {
|
||||
goto end
|
||||
}
|
||||
r.AdvanceLine()
|
||||
if ret == nil {
|
||||
ret = NewSegments()
|
||||
}
|
||||
ret.Append(seg)
|
||||
}
|
||||
end:
|
||||
if !opts.Advance {
|
||||
r.SetPosition(orgline, orgpos)
|
||||
}
|
||||
if closed {
|
||||
return ret, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
|
|
|||
16
text/reader_test.go
Normal file
16
text/reader_test.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package text
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFindSubMatchReader(t *testing.T) {
|
||||
s := "微笑"
|
||||
r := NewReader([]byte(":" + s + ":"))
|
||||
reg := regexp.MustCompile(`:(\p{L}+):`)
|
||||
match := r.FindSubMatch(reg)
|
||||
if len(match) != 2 || string(match[1]) != s {
|
||||
t.Fatal("no match cjk")
|
||||
}
|
||||
}
|
||||
|
|
@ -2,12 +2,13 @@ package text
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
var space = []byte(" ")
|
||||
|
||||
// A Segment struct holds information about source potisions.
|
||||
// A Segment struct holds information about source positions.
|
||||
type Segment struct {
|
||||
// Start is a start position of the segment.
|
||||
Start int
|
||||
|
|
@ -18,6 +19,20 @@ type Segment struct {
|
|||
|
||||
// Padding is a padding length of the segment.
|
||||
Padding int
|
||||
|
||||
// ForceNewline is true if the segment should be ended with a newline.
|
||||
// Some elements(i.e. CodeBlock, FencedCodeBlock) does not trim trailing
|
||||
// newlines. Spec defines that EOF is treated as a newline, so we need to
|
||||
// add a newline to the end of the segment if it is not empty.
|
||||
//
|
||||
// i.e.:
|
||||
//
|
||||
// ```go
|
||||
// const test = "test"
|
||||
//
|
||||
// This code does not close the code block and ends with EOF. In this case,
|
||||
// we need to add a newline to the end of the last line like `const test = "test"\n`.
|
||||
ForceNewline bool
|
||||
}
|
||||
|
||||
// NewSegment return a new Segment.
|
||||
|
|
@ -40,12 +55,18 @@ func NewSegmentPadding(start, stop, n int) Segment {
|
|||
|
||||
// Value returns a value of the segment.
|
||||
func (t *Segment) Value(buffer []byte) []byte {
|
||||
var result []byte
|
||||
if t.Padding == 0 {
|
||||
return buffer[t.Start:t.Stop]
|
||||
result = buffer[t.Start:t.Stop]
|
||||
} else {
|
||||
result = make([]byte, 0, t.Padding+t.Stop-t.Start+1)
|
||||
result = append(result, bytes.Repeat(space, t.Padding)...)
|
||||
result = append(result, buffer[t.Start:t.Stop]...)
|
||||
}
|
||||
result := make([]byte, 0, t.Padding+t.Stop-t.Start+1)
|
||||
result = append(result, bytes.Repeat(space, t.Padding)...)
|
||||
return append(result, buffer[t.Start:t.Stop]...)
|
||||
if t.ForceNewline && len(result) > 0 && result[len(result)-1] != '\n' {
|
||||
result = append(result, '\n')
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Len returns a length of the segment.
|
||||
|
|
@ -197,7 +218,7 @@ func (s *Segments) Sliced(lo, hi int) []Segment {
|
|||
return s.values[lo:hi]
|
||||
}
|
||||
|
||||
// Clear delete all element of the collction.
|
||||
// Clear delete all element of the collection.
|
||||
func (s *Segments) Clear() {
|
||||
s.values = nil
|
||||
}
|
||||
|
|
@ -207,3 +228,12 @@ func (s *Segments) Unshift(v Segment) {
|
|||
s.values = append(s.values[0:1], s.values[0:]...)
|
||||
s.values[0] = v
|
||||
}
|
||||
|
||||
// Value returns a string value of the collection.
|
||||
func (s *Segments) Value(buffer []byte) []byte {
|
||||
var result []byte
|
||||
for _, v := range s.values {
|
||||
result = append(result, v.Value(buffer)...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
1535
util/unicode_case_folding.go
Normal file
1535
util/unicode_case_folding.go
Normal file
File diff suppressed because it is too large
Load diff
622
util/util.go
622
util/util.go
|
|
@ -8,7 +8,7 @@ import (
|
|||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
|
|
@ -28,6 +28,7 @@ func NewCopyOnWriteBuffer(buffer []byte) CopyOnWriteBuffer {
|
|||
}
|
||||
|
||||
// Write writes given bytes to the buffer.
|
||||
// Write allocate new buffer and clears it at the first time.
|
||||
func (b *CopyOnWriteBuffer) Write(value []byte) {
|
||||
if !b.copied {
|
||||
b.buffer = make([]byte, 0, len(b.buffer)+20)
|
||||
|
|
@ -36,13 +37,51 @@ func (b *CopyOnWriteBuffer) Write(value []byte) {
|
|||
b.buffer = append(b.buffer, value...)
|
||||
}
|
||||
|
||||
// WriteString writes given string to the buffer.
|
||||
// WriteString allocate new buffer and clears it at the first time.
|
||||
func (b *CopyOnWriteBuffer) WriteString(value string) {
|
||||
b.Write(StringToReadOnlyBytes(value))
|
||||
}
|
||||
|
||||
// Append appends given bytes to the buffer.
|
||||
// Append copy buffer at the first time.
|
||||
func (b *CopyOnWriteBuffer) Append(value []byte) {
|
||||
if !b.copied {
|
||||
tmp := make([]byte, len(b.buffer), len(b.buffer)+20)
|
||||
copy(tmp, b.buffer)
|
||||
b.buffer = tmp
|
||||
b.copied = true
|
||||
}
|
||||
b.buffer = append(b.buffer, value...)
|
||||
}
|
||||
|
||||
// AppendString appends given string to the buffer.
|
||||
// AppendString copy buffer at the first time.
|
||||
func (b *CopyOnWriteBuffer) AppendString(value string) {
|
||||
b.Append(StringToReadOnlyBytes(value))
|
||||
}
|
||||
|
||||
// WriteByte writes the given byte to the buffer.
|
||||
func (b *CopyOnWriteBuffer) WriteByte(c byte) {
|
||||
// WriteByte allocate new buffer and clears it at the first time.
|
||||
func (b *CopyOnWriteBuffer) WriteByte(c byte) error {
|
||||
if !b.copied {
|
||||
b.buffer = make([]byte, 0, len(b.buffer)+20)
|
||||
b.copied = true
|
||||
}
|
||||
b.buffer = append(b.buffer, c)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AppendByte appends given bytes to the buffer.
|
||||
// AppendByte copy buffer at the first time.
|
||||
func (b *CopyOnWriteBuffer) AppendByte(c byte) {
|
||||
if !b.copied {
|
||||
tmp := make([]byte, len(b.buffer), len(b.buffer)+20)
|
||||
copy(tmp, b.buffer)
|
||||
b.buffer = tmp
|
||||
b.copied = true
|
||||
}
|
||||
b.buffer = append(b.buffer, c)
|
||||
}
|
||||
|
||||
// Bytes returns bytes of this buffer.
|
||||
|
|
@ -55,7 +94,7 @@ func (b *CopyOnWriteBuffer) IsCopied() bool {
|
|||
return b.copied
|
||||
}
|
||||
|
||||
// IsEscapedPunctuation returns true if caracter at a given index i
|
||||
// IsEscapedPunctuation returns true if character at a given index i
|
||||
// is an escaped punctuation, otherwise false.
|
||||
func IsEscapedPunctuation(source []byte, i int) bool {
|
||||
return source[i] == '\\' && i < len(source)-1 && IsPunct(source[i+1])
|
||||
|
|
@ -79,44 +118,22 @@ func ReadWhile(source []byte, index [2]int, pred func(byte) bool) (int, bool) {
|
|||
// IsBlank returns true if the given string is all space characters.
|
||||
func IsBlank(bs []byte) bool {
|
||||
for _, b := range bs {
|
||||
if IsSpace(b) {
|
||||
continue
|
||||
if !IsSpace(b) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// DedentPosition dedents lines by the given width.
|
||||
func DedentPosition(bs []byte, width int) (pos, padding int) {
|
||||
if width == 0 {
|
||||
return
|
||||
}
|
||||
i := 0
|
||||
l := len(bs)
|
||||
w := 0
|
||||
for ; i < l && w < width; i++ {
|
||||
b := bs[i]
|
||||
if b == ' ' {
|
||||
w++
|
||||
} else if b == '\t' {
|
||||
w += 4
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
padding = w - width
|
||||
if padding < 0 {
|
||||
padding = 0
|
||||
}
|
||||
return i, padding
|
||||
}
|
||||
|
||||
// VisualizeSpaces visualize invisible space characters.
|
||||
func VisualizeSpaces(bs []byte) []byte {
|
||||
bs = bytes.Replace(bs, []byte(" "), []byte("[SPACE]"), -1)
|
||||
bs = bytes.Replace(bs, []byte("\t"), []byte("[TAB]"), -1)
|
||||
bs = bytes.Replace(bs, []byte("\n"), []byte("[NEWLINE]\n"), -1)
|
||||
bs = bytes.Replace(bs, []byte("\r"), []byte("[CR]"), -1)
|
||||
bs = bytes.Replace(bs, []byte("\v"), []byte("[VTAB]"), -1)
|
||||
bs = bytes.Replace(bs, []byte("\x00"), []byte("[NUL]"), -1)
|
||||
bs = bytes.Replace(bs, []byte("\ufffd"), []byte("[U+FFFD]"), -1)
|
||||
return bs
|
||||
}
|
||||
|
||||
|
|
@ -129,31 +146,100 @@ func TabWidth(currentPos int) int {
|
|||
// If the line contains tab characters, paddings may be not zero.
|
||||
// currentPos==0 and width==2:
|
||||
//
|
||||
// position: 0 1
|
||||
// [TAB]aaaa
|
||||
// width: 1234 5678
|
||||
// position: 0 1
|
||||
// [TAB]aaaa
|
||||
// width: 1234 5678
|
||||
//
|
||||
// width=2 is in the tab character. In this case, IndentPosition returns
|
||||
// (pos=1, padding=2)
|
||||
// (pos=1, padding=2).
|
||||
func IndentPosition(bs []byte, currentPos, width int) (pos, padding int) {
|
||||
return IndentPositionPadding(bs, currentPos, 0, width)
|
||||
}
|
||||
|
||||
// IndentPositionPadding searches an indent position with the given width for the given line.
|
||||
// This function is mostly same as IndentPosition except this function
|
||||
// takes account into additional paddings.
|
||||
func IndentPositionPadding(bs []byte, currentPos, paddingv, width int) (pos, padding int) {
|
||||
if width == 0 {
|
||||
return 0, paddingv
|
||||
}
|
||||
w := 0
|
||||
i := 0
|
||||
l := len(bs)
|
||||
for i := 0; i < l; i++ {
|
||||
b := bs[i]
|
||||
if b == ' ' {
|
||||
p := paddingv
|
||||
for ; i < l; i++ {
|
||||
if p > 0 {
|
||||
p--
|
||||
w++
|
||||
} else if b == '\t' {
|
||||
continue
|
||||
}
|
||||
if bs[i] == '\t' && w < width {
|
||||
w += TabWidth(currentPos + w)
|
||||
} else if bs[i] == ' ' && w < width {
|
||||
w++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
if w >= width {
|
||||
return i + 1, w - width
|
||||
}
|
||||
}
|
||||
if w >= width {
|
||||
return i - paddingv, w - width
|
||||
}
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
// DedentPosition dedents lines by the given width.
|
||||
//
|
||||
// Deprecated: This function has bugs. Use util.IndentPositionPadding and util.FirstNonSpacePosition.
|
||||
func DedentPosition(bs []byte, currentPos, width int) (pos, padding int) {
|
||||
if width == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
w := 0
|
||||
l := len(bs)
|
||||
i := 0
|
||||
for ; i < l; i++ {
|
||||
if bs[i] == '\t' {
|
||||
w += TabWidth(currentPos + w)
|
||||
} else if bs[i] == ' ' {
|
||||
w++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if w >= width {
|
||||
return i, w - width
|
||||
}
|
||||
return i, 0
|
||||
}
|
||||
|
||||
// DedentPositionPadding dedents lines by the given width.
|
||||
// This function is mostly same as DedentPosition except this function
|
||||
// takes account into additional paddings.
|
||||
//
|
||||
// Deprecated: This function has bugs. Use util.IndentPositionPadding and util.FirstNonSpacePosition.
|
||||
func DedentPositionPadding(bs []byte, currentPos, paddingv, width int) (pos, padding int) {
|
||||
if width == 0 {
|
||||
return 0, paddingv
|
||||
}
|
||||
|
||||
w := 0
|
||||
i := 0
|
||||
l := len(bs)
|
||||
for ; i < l; i++ {
|
||||
if bs[i] == '\t' {
|
||||
w += TabWidth(currentPos + w)
|
||||
} else if bs[i] == ' ' {
|
||||
w++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if w >= width {
|
||||
return i - paddingv, w - width
|
||||
}
|
||||
return i - paddingv, 0
|
||||
}
|
||||
|
||||
// IndentWidth calculate an indent width for the given line.
|
||||
func IndentWidth(bs []byte, currentPos int) (width, pos int) {
|
||||
l := len(bs)
|
||||
|
|
@ -172,7 +258,7 @@ func IndentWidth(bs []byte, currentPos int) (width, pos int) {
|
|||
return
|
||||
}
|
||||
|
||||
// FirstNonSpacePosition returns a potisoin line that is a first nonspace
|
||||
// FirstNonSpacePosition returns a position line that is a first nonspace
|
||||
// character.
|
||||
func FirstNonSpacePosition(bs []byte) int {
|
||||
i := 0
|
||||
|
|
@ -193,6 +279,10 @@ func FirstNonSpacePosition(bs []byte) int {
|
|||
// If codeSpan is set true, it ignores characters in code spans.
|
||||
// If allowNesting is set true, closures correspond to nested opener will be
|
||||
// ignored.
|
||||
//
|
||||
// Deprecated: This function can not handle newlines. Many elements
|
||||
// can be existed over multiple lines(e.g. link labels).
|
||||
// Use text.Reader.FindClosure.
|
||||
func FindClosure(bs []byte, opener, closure byte, codeSpan, allowNesting bool) int {
|
||||
i := 0
|
||||
opened := 1
|
||||
|
|
@ -205,13 +295,14 @@ func FindClosure(bs []byte, opener, closure byte, codeSpan, allowNesting bool) i
|
|||
if bs[i] == '`' {
|
||||
codeSpanCloser++
|
||||
} else {
|
||||
i--
|
||||
break
|
||||
}
|
||||
}
|
||||
if codeSpanCloser == codeSpanOpener {
|
||||
codeSpanOpener = 0
|
||||
}
|
||||
} else if c == '\\' && i < len(bs)-1 && IsPunct(bs[i+1]) {
|
||||
} else if codeSpanOpener == 0 && c == '\\' && i < len(bs)-1 && IsPunct(bs[i+1]) {
|
||||
i += 2
|
||||
continue
|
||||
} else if codeSpan && codeSpanOpener == 0 && c == '`' {
|
||||
|
|
@ -219,6 +310,7 @@ func FindClosure(bs []byte, opener, closure byte, codeSpan, allowNesting bool) i
|
|||
if bs[i] == '`' {
|
||||
codeSpanOpener++
|
||||
} else {
|
||||
i--
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -328,6 +420,52 @@ func TrimRightSpace(source []byte) []byte {
|
|||
return TrimRight(source, spaces)
|
||||
}
|
||||
|
||||
// DoFullUnicodeCaseFolding performs full unicode case folding to given bytes.
|
||||
func DoFullUnicodeCaseFolding(v []byte) []byte {
|
||||
var rbuf []byte
|
||||
cob := NewCopyOnWriteBuffer(v)
|
||||
n := 0
|
||||
for i := 0; i < len(v); i++ {
|
||||
c := v[i]
|
||||
if c < 0xb5 {
|
||||
if c >= 0x41 && c <= 0x5a {
|
||||
// A-Z to a-z
|
||||
cob.Write(v[n:i])
|
||||
_ = cob.WriteByte(c + 32)
|
||||
n = i + 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !utf8.RuneStart(c) {
|
||||
continue
|
||||
}
|
||||
r, length := utf8.DecodeRune(v[i:])
|
||||
if r == utf8.RuneError {
|
||||
continue
|
||||
}
|
||||
folded, ok := unicodeCaseFoldings[r]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
cob.Write(v[n:i])
|
||||
if rbuf == nil {
|
||||
rbuf = make([]byte, 4)
|
||||
}
|
||||
for _, f := range folded {
|
||||
l := utf8.EncodeRune(rbuf, f)
|
||||
cob.Write(rbuf[:l])
|
||||
}
|
||||
i += length - 1
|
||||
n = i + 1
|
||||
}
|
||||
if cob.IsCopied() {
|
||||
cob.Write(v[n:])
|
||||
}
|
||||
return cob.Bytes()
|
||||
}
|
||||
|
||||
// ReplaceSpaces replaces sequence of spaces with the given repl.
|
||||
func ReplaceSpaces(source []byte, repl byte) []byte {
|
||||
var ret []byte
|
||||
|
|
@ -380,16 +518,17 @@ func ToValidRune(v rune) rune {
|
|||
return v
|
||||
}
|
||||
|
||||
// ToLinkReference convert given bytes into a valid link reference string.
|
||||
// ToLinkReference trims leading and trailing spaces and convert into lower
|
||||
// ToLinkReference converts given bytes into a valid link reference string.
|
||||
// ToLinkReference performs unicode case folding, trims leading and trailing spaces, converts into lower
|
||||
// case and replace spaces with a single space character.
|
||||
func ToLinkReference(v []byte) string {
|
||||
v = TrimLeftSpace(v)
|
||||
v = TrimRightSpace(v)
|
||||
return strings.ToLower(string(ReplaceSpaces(v, ' ')))
|
||||
v = DoFullUnicodeCaseFolding(v)
|
||||
return string(ReplaceSpaces(v, ' '))
|
||||
}
|
||||
|
||||
var htmlEscapeTable = [256][]byte{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, []byte("""), nil, nil, nil, []byte("&"), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, []byte("<"), nil, []byte(">"), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil}
|
||||
var htmlEscapeTable = [256][]byte{nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, []byte("""), nil, nil, nil, []byte("&"), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, []byte("<"), nil, []byte(">"), nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil} //nolint:golint,lll
|
||||
|
||||
// EscapeHTMLByte returns HTML escaped bytes if the given byte should be escaped,
|
||||
// otherwise nil.
|
||||
|
|
@ -411,7 +550,7 @@ func EscapeHTML(v []byte) []byte {
|
|||
}
|
||||
}
|
||||
if cob.IsCopied() {
|
||||
cob.Write(v[n:len(v)])
|
||||
cob.Write(v[n:])
|
||||
}
|
||||
return cob.Bytes()
|
||||
}
|
||||
|
|
@ -425,7 +564,7 @@ func UnescapePunctuations(source []byte) []byte {
|
|||
c := source[i]
|
||||
if i < limit-1 && c == '\\' && IsPunct(source[i+1]) {
|
||||
cob.Write(source[n:i])
|
||||
cob.WriteByte(source[i+1])
|
||||
_ = cob.WriteByte(source[i+1])
|
||||
i += 2
|
||||
n = i
|
||||
continue
|
||||
|
|
@ -433,7 +572,7 @@ func UnescapePunctuations(source []byte) []byte {
|
|||
i++
|
||||
}
|
||||
if cob.IsCopied() {
|
||||
cob.Write(source[n:len(source)])
|
||||
cob.Write(source[n:])
|
||||
}
|
||||
return cob.Bytes()
|
||||
}
|
||||
|
|
@ -441,9 +580,9 @@ func UnescapePunctuations(source []byte) []byte {
|
|||
// ResolveNumericReferences resolve numeric references like 'Ӓ" .
|
||||
func ResolveNumericReferences(source []byte) []byte {
|
||||
cob := NewCopyOnWriteBuffer(source)
|
||||
buf := make([]byte, 6, 6)
|
||||
buf := make([]byte, 6)
|
||||
limit := len(source)
|
||||
ok := false
|
||||
var ok bool
|
||||
n := 0
|
||||
for i := 0; i < limit; i++ {
|
||||
if source[i] == '&' {
|
||||
|
|
@ -451,30 +590,32 @@ func ResolveNumericReferences(source []byte) []byte {
|
|||
next := i + 1
|
||||
if next < limit && source[next] == '#' {
|
||||
nnext := next + 1
|
||||
nc := source[nnext]
|
||||
// code point like #x22;
|
||||
if nnext < limit && nc == 'x' || nc == 'X' {
|
||||
start := nnext + 1
|
||||
i, ok = ReadWhile(source, [2]int{start, limit}, IsHexDecimal)
|
||||
if ok && i < limit && source[i] == ';' {
|
||||
v, _ := strconv.ParseUint(BytesToReadOnlyString(source[start:i]), 16, 32)
|
||||
cob.Write(source[n:pos])
|
||||
n = i + 1
|
||||
runeSize := utf8.EncodeRune(buf, ToValidRune(rune(v)))
|
||||
cob.Write(buf[:runeSize])
|
||||
continue
|
||||
}
|
||||
// code point like #1234;
|
||||
} else if nc >= '0' && nc <= '9' {
|
||||
start := nnext
|
||||
i, ok = ReadWhile(source, [2]int{start, limit}, IsNumeric)
|
||||
if ok && i < limit && i-start < 8 && source[i] == ';' {
|
||||
v, _ := strconv.ParseUint(BytesToReadOnlyString(source[start:i]), 0, 32)
|
||||
cob.Write(source[n:pos])
|
||||
n = i + 1
|
||||
runeSize := utf8.EncodeRune(buf, ToValidRune(rune(v)))
|
||||
cob.Write(buf[:runeSize])
|
||||
continue
|
||||
if nnext < limit {
|
||||
nc := source[nnext]
|
||||
// code point like #x22;
|
||||
if nnext < limit && nc == 'x' || nc == 'X' {
|
||||
start := nnext + 1
|
||||
i, ok = ReadWhile(source, [2]int{start, limit}, IsHexDecimal)
|
||||
if ok && i < limit && source[i] == ';' {
|
||||
v, _ := strconv.ParseUint(BytesToReadOnlyString(source[start:i]), 16, 32)
|
||||
cob.Write(source[n:pos])
|
||||
n = i + 1
|
||||
runeSize := utf8.EncodeRune(buf, ToValidRune(rune(v)))
|
||||
cob.Write(buf[:runeSize])
|
||||
continue
|
||||
}
|
||||
// code point like #1234;
|
||||
} else if nc >= '0' && nc <= '9' {
|
||||
start := nnext
|
||||
i, ok = ReadWhile(source, [2]int{start, limit}, IsNumeric)
|
||||
if ok && i < limit && i-start < 8 && source[i] == ';' {
|
||||
v, _ := strconv.ParseUint(BytesToReadOnlyString(source[start:i]), 0, 32)
|
||||
cob.Write(source[n:pos])
|
||||
n = i + 1
|
||||
runeSize := utf8.EncodeRune(buf, ToValidRune(rune(v)))
|
||||
cob.Write(buf[:runeSize])
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -482,7 +623,7 @@ func ResolveNumericReferences(source []byte) []byte {
|
|||
}
|
||||
}
|
||||
if cob.IsCopied() {
|
||||
cob.Write(source[n:len(source)])
|
||||
cob.Write(source[n:])
|
||||
}
|
||||
return cob.Bytes()
|
||||
}
|
||||
|
|
@ -491,7 +632,7 @@ func ResolveNumericReferences(source []byte) []byte {
|
|||
func ResolveEntityNames(source []byte) []byte {
|
||||
cob := NewCopyOnWriteBuffer(source)
|
||||
limit := len(source)
|
||||
ok := false
|
||||
var ok bool
|
||||
n := 0
|
||||
for i := 0; i < limit; i++ {
|
||||
if source[i] == '&' {
|
||||
|
|
@ -515,7 +656,7 @@ func ResolveEntityNames(source []byte) []byte {
|
|||
}
|
||||
}
|
||||
if cob.IsCopied() {
|
||||
cob.Write(source[n:len(source)])
|
||||
cob.Write(source[n:])
|
||||
}
|
||||
return cob.Bytes()
|
||||
}
|
||||
|
|
@ -524,11 +665,11 @@ var htmlSpace = []byte("%20")
|
|||
|
||||
// URLEscape escape the given URL.
|
||||
// If resolveReference is set true:
|
||||
// 1. unescape punctuations
|
||||
// 2. resolve numeric references
|
||||
// 3. resolve entity references
|
||||
// 1. unescape punctuations
|
||||
// 2. resolve numeric references
|
||||
// 3. resolve entity references
|
||||
//
|
||||
// URL encoded values (%xx) are keeped as is.
|
||||
// URL encoded values (%xx) are kept as is.
|
||||
func URLEscape(v []byte, resolveReference bool) []byte {
|
||||
if resolveReference {
|
||||
v = UnescapePunctuations(v)
|
||||
|
|
@ -561,184 +702,31 @@ func URLEscape(v []byte, resolveReference bool) []byte {
|
|||
n = i
|
||||
continue
|
||||
}
|
||||
if int(u8len) > len(v) {
|
||||
u8len = int8(len(v) - 1)
|
||||
}
|
||||
if u8len == 0 {
|
||||
i++
|
||||
n = i
|
||||
continue
|
||||
}
|
||||
cob.Write(v[n:i])
|
||||
cob.Write(StringToReadOnlyBytes(url.QueryEscape(string(v[i : i+int(u8len)]))))
|
||||
stop := i + int(u8len)
|
||||
if stop > len(v) {
|
||||
i++
|
||||
n = i
|
||||
continue
|
||||
}
|
||||
cob.Write(StringToReadOnlyBytes(url.QueryEscape(string(v[i:stop]))))
|
||||
i += int(u8len)
|
||||
n = i
|
||||
}
|
||||
if cob.IsCopied() {
|
||||
cob.Write(v[n:len(v)])
|
||||
if cob.IsCopied() && n < limit {
|
||||
cob.Write(v[n:])
|
||||
}
|
||||
return cob.Bytes()
|
||||
}
|
||||
|
||||
// FindAttributeIndiciesReverse searches attribute indicies from tail of the given
|
||||
// bytes and returns indicies.
|
||||
func FindAttributeIndiciesReverse(b []byte, canEscapeQuotes bool) [][4]int {
|
||||
i := 0
|
||||
retry:
|
||||
var result [][4]int
|
||||
as := -1
|
||||
for i < len(b) {
|
||||
if IsEscapedPunctuation(b, i) {
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if b[i] == '{' {
|
||||
i++
|
||||
as = i
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if as < 0 {
|
||||
return nil
|
||||
}
|
||||
for as < len(b) {
|
||||
ai := FindAttributeIndex(b[as:], canEscapeQuotes)
|
||||
if ai[0] < 0 {
|
||||
break
|
||||
}
|
||||
i = as + ai[3]
|
||||
if result == nil {
|
||||
result = [][4]int{}
|
||||
}
|
||||
result = append(result, [4]int{as + ai[0], as + ai[1], as + ai[2], as + ai[3]})
|
||||
as += ai[3]
|
||||
}
|
||||
if b[as] == '}' && (as > len(b)-2 || IsBlank(b[as:])) {
|
||||
return result
|
||||
}
|
||||
goto retry
|
||||
}
|
||||
|
||||
// FindAttributeIndex searchs
|
||||
// - #id
|
||||
// - .class
|
||||
// - attr=value
|
||||
// in given bytes.
|
||||
// FindHTMLAttributeIndex returns an int array that elements are
|
||||
// [name_start, name_stop, value_start, value_stop].
|
||||
// value_start and value_stop does not include " or '.
|
||||
// If no attributes found, it returns [4]int{-1, -1, -1, -1}.
|
||||
func FindAttributeIndex(b []byte, canEscapeQuotes bool) [4]int {
|
||||
result := [4]int{-1, -1, -1, -1}
|
||||
i := 0
|
||||
l := len(b)
|
||||
for ; i < l && IsSpace(b[i]); i++ {
|
||||
}
|
||||
if i >= l {
|
||||
return result
|
||||
}
|
||||
c := b[i]
|
||||
if c == '#' || c == '.' {
|
||||
result[0] = i
|
||||
i++
|
||||
result[1] = i
|
||||
result[2] = i
|
||||
for ; i < l && !IsSpace(b[i]) && (!IsPunct(b[i]) || b[i] == '_' || b[i] == '-'); i++ {
|
||||
}
|
||||
result[3] = i
|
||||
return result
|
||||
}
|
||||
return FindHTMLAttributeIndex(b, canEscapeQuotes)
|
||||
}
|
||||
|
||||
// FindHTMLAttributeIndex searches HTML attributes in given bytes.
|
||||
// FindHTMLAttributeIndex returns an int array that elements are
|
||||
// [name_start, name_stop, value_start, value_stop].
|
||||
// value_start and value_stop does not include " or '.
|
||||
// If no attributes found, it returns [4]int{-1, -1, -1, -1}.
|
||||
func FindHTMLAttributeIndex(b []byte, canEscapeQuotes bool) [4]int {
|
||||
result := [4]int{-1, -1, -1, -1}
|
||||
i := 0
|
||||
l := len(b)
|
||||
for ; i < l && IsSpace(b[i]); i++ {
|
||||
}
|
||||
if i >= l {
|
||||
return result
|
||||
}
|
||||
c := b[i]
|
||||
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
c == '_' || c == ':') {
|
||||
return result
|
||||
}
|
||||
result[0] = i
|
||||
for ; i < l; i++ {
|
||||
c := b[i]
|
||||
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') ||
|
||||
c == '_' || c == ':' || c == '.' || c == '-') {
|
||||
break
|
||||
}
|
||||
}
|
||||
result[1] = i
|
||||
for ; i < l && IsSpace(b[i]); i++ {
|
||||
}
|
||||
if i >= l {
|
||||
return [4]int{-1, -1, -1, -1}
|
||||
}
|
||||
if b[i] != '=' {
|
||||
return [4]int{-1, -1, -1, -1}
|
||||
}
|
||||
i++
|
||||
for ; i < l && IsSpace(b[i]); i++ {
|
||||
}
|
||||
if i >= l {
|
||||
return [4]int{-1, -1, -1, -1}
|
||||
}
|
||||
if b[i] == '"' {
|
||||
i++
|
||||
result[2] = i
|
||||
if canEscapeQuotes {
|
||||
pos := FindClosure(b[i:], '"', '"', false, false)
|
||||
if pos < 0 {
|
||||
return [4]int{-1, -1, -1, -1}
|
||||
}
|
||||
result[3] = pos + i
|
||||
} else {
|
||||
for ; i < l && b[i] != '"'; i++ {
|
||||
}
|
||||
result[3] = i
|
||||
if result[2] == result[3] || i == l && b[l-1] != '"' {
|
||||
return [4]int{-1, -1, -1, -1}
|
||||
}
|
||||
}
|
||||
} else if b[i] == '\'' {
|
||||
i++
|
||||
result[2] = i
|
||||
if canEscapeQuotes {
|
||||
pos := FindClosure(b[i:], '\'', '\'', false, false)
|
||||
if pos < 0 {
|
||||
return [4]int{-1, -1, -1, -1}
|
||||
}
|
||||
result[3] = pos + i
|
||||
} else {
|
||||
for ; i < l && b[i] != '\''; i++ {
|
||||
}
|
||||
result[3] = i
|
||||
if result[2] == result[3] || i == l && b[l-1] != '\'' {
|
||||
return [4]int{-1, -1, -1, -1}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result[2] = i
|
||||
for ; i < l; i++ {
|
||||
c = b[i]
|
||||
if c == '\\' || c == '"' || c == '\'' ||
|
||||
c == '=' || c == '<' || c == '>' || c == '`' ||
|
||||
(c >= 0 && c <= 0x20) {
|
||||
break
|
||||
}
|
||||
}
|
||||
result[3] = i
|
||||
if result[2] == result[3] {
|
||||
return [4]int{-1, -1, -1, -1}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FindURLIndex returns a stop index value if the given bytes seem an URL.
|
||||
// This function is equivalent to [A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]* .
|
||||
func FindURLIndex(b []byte) int {
|
||||
|
|
@ -769,7 +757,7 @@ func FindURLIndex(b []byte) int {
|
|||
return i
|
||||
}
|
||||
|
||||
var emailDomainRegexp = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*`)
|
||||
var emailDomainRegexp = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*`) //nolint:golint,lll
|
||||
|
||||
// FindEmailIndex returns a stop index value if the given bytes seem an email address.
|
||||
func FindEmailIndex(b []byte) int {
|
||||
|
|
@ -800,18 +788,19 @@ func FindEmailIndex(b []byte) int {
|
|||
|
||||
var spaces = []byte(" \t\n\x0b\x0c\x0d")
|
||||
|
||||
var spaceTable = [256]int8{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
var spaceTable = [256]int8{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} //nolint:golint,lll
|
||||
|
||||
var punctTable = [256]int8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
var punctTable = [256]int8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} //nolint:golint,lll
|
||||
|
||||
// a-zA-Z0-9, ;/?:@&=+$,-_.!~*'()#
|
||||
var urlEscapeTable = [256]int8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
|
||||
var utf8lenTable = [256]int8{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 99, 99, 99, 99, 99, 99, 99, 99}
|
||||
var urlEscapeTable = [256]int8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} //nolint:golint,lll
|
||||
|
||||
var urlTable = [256]uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 5, 5, 1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 0, 1, 0, 1, 1, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
|
||||
var utf8lenTable = [256]int8{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 99, 99, 99, 99, 99, 99, 99, 99} //nolint:golint,lll
|
||||
|
||||
var emailTable = [256]uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
var urlTable = [256]uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 5, 5, 1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 0, 1, 0, 1, 1, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} //nolint:golint,lll
|
||||
|
||||
var emailTable = [256]uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} //nolint:golint,lll
|
||||
|
||||
// UTF8Len returns a byte length of the utf-8 character.
|
||||
func UTF8Len(b byte) int8 {
|
||||
|
|
@ -823,11 +812,21 @@ func IsPunct(c byte) bool {
|
|||
return punctTable[c] == 1
|
||||
}
|
||||
|
||||
// IsPunctRune returns true if the given rune is a punctuation, otherwise false.
|
||||
func IsPunctRune(r rune) bool {
|
||||
return unicode.IsSymbol(r) || unicode.IsPunct(r)
|
||||
}
|
||||
|
||||
// IsSpace returns true if the given character is a space, otherwise false.
|
||||
func IsSpace(c byte) bool {
|
||||
return spaceTable[c] == 1
|
||||
}
|
||||
|
||||
// IsSpaceRune returns true if the given rune is a space, otherwise false.
|
||||
func IsSpaceRune(r rune) bool {
|
||||
return int32(r) <= 256 && IsSpace(byte(r)) || unicode.IsSpace(r)
|
||||
}
|
||||
|
||||
// IsNumeric returns true if the given character is a numeric, otherwise false.
|
||||
func IsNumeric(c byte) bool {
|
||||
return c >= '0' && c <= '9'
|
||||
|
|
@ -854,15 +853,15 @@ type BufWriter interface {
|
|||
WriteString(s string) (int, error)
|
||||
}
|
||||
|
||||
// A PrioritizedValue struct holds pair of an arbitary value and a priority.
|
||||
// A PrioritizedValue struct holds pair of an arbitrary value and a priority.
|
||||
type PrioritizedValue struct {
|
||||
// Value is an arbitary value that you want to prioritize.
|
||||
// Value is an arbitrary value that you want to prioritize.
|
||||
Value interface{}
|
||||
// Priority is a priority of the value.
|
||||
Priority int
|
||||
}
|
||||
|
||||
// PrioritizedSlice is a slice of the PrioritizedValues
|
||||
// PrioritizedSlice is a slice of the PrioritizedValues.
|
||||
type PrioritizedSlice []PrioritizedValue
|
||||
|
||||
// Sort sorts the PrioritizedSlice in ascending order.
|
||||
|
|
@ -892,3 +891,98 @@ func (s PrioritizedSlice) Remove(v interface{}) PrioritizedSlice {
|
|||
func Prioritized(v interface{}, priority int) PrioritizedValue {
|
||||
return PrioritizedValue{v, priority}
|
||||
}
|
||||
|
||||
func bytesHash(b []byte) uint64 {
|
||||
var hash uint64 = 5381
|
||||
for _, c := range b {
|
||||
hash = ((hash << 5) + hash) + uint64(c)
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
// BytesFilter is a efficient data structure for checking whether bytes exist or not.
|
||||
// BytesFilter is thread-safe.
|
||||
type BytesFilter interface {
|
||||
// Add adds given bytes to this set.
|
||||
Add([]byte)
|
||||
|
||||
// Contains return true if this set contains given bytes, otherwise false.
|
||||
Contains([]byte) bool
|
||||
|
||||
// Extend copies this filter and adds given bytes to new filter.
|
||||
Extend(...[]byte) BytesFilter
|
||||
}
|
||||
|
||||
type bytesFilter struct {
|
||||
chars [256]uint8
|
||||
threshold int
|
||||
slots [][][]byte
|
||||
}
|
||||
|
||||
// NewBytesFilter returns a new BytesFilter.
|
||||
func NewBytesFilter(elements ...[]byte) BytesFilter {
|
||||
s := &bytesFilter{
|
||||
threshold: 3,
|
||||
slots: make([][][]byte, 64),
|
||||
}
|
||||
for _, element := range elements {
|
||||
s.Add(element)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *bytesFilter) Add(b []byte) {
|
||||
l := len(b)
|
||||
m := s.threshold
|
||||
if l < s.threshold {
|
||||
m = l
|
||||
}
|
||||
for i := 0; i < m; i++ {
|
||||
s.chars[b[i]] |= 1 << uint8(i)
|
||||
}
|
||||
h := bytesHash(b) % uint64(len(s.slots))
|
||||
slot := s.slots[h]
|
||||
if slot == nil {
|
||||
slot = [][]byte{}
|
||||
}
|
||||
s.slots[h] = append(slot, b)
|
||||
}
|
||||
|
||||
func (s *bytesFilter) Extend(bs ...[]byte) BytesFilter {
|
||||
newFilter := NewBytesFilter().(*bytesFilter)
|
||||
newFilter.chars = s.chars
|
||||
newFilter.threshold = s.threshold
|
||||
for k, v := range s.slots {
|
||||
newSlot := make([][]byte, len(v))
|
||||
copy(newSlot, v)
|
||||
newFilter.slots[k] = v
|
||||
}
|
||||
for _, b := range bs {
|
||||
newFilter.Add(b)
|
||||
}
|
||||
return newFilter
|
||||
}
|
||||
|
||||
func (s *bytesFilter) Contains(b []byte) bool {
|
||||
l := len(b)
|
||||
m := s.threshold
|
||||
if l < s.threshold {
|
||||
m = l
|
||||
}
|
||||
for i := 0; i < m; i++ {
|
||||
if (s.chars[b[i]] & (1 << uint8(i))) == 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
h := bytesHash(b) % uint64(len(s.slots))
|
||||
slot := s.slots[h]
|
||||
if len(slot) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, element := range slot {
|
||||
if bytes.Equal(element, b) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
469
util/util_cjk.go
Normal file
469
util/util_cjk.go
Normal file
|
|
@ -0,0 +1,469 @@
|
|||
package util
|
||||
|
||||
import "unicode"
|
||||
|
||||
var cjkRadicalsSupplement = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0x2E80, 0x2EFF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var kangxiRadicals = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0x2F00, 0x2FDF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var ideographicDescriptionCharacters = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0x2FF0, 0x2FFF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var cjkSymbolsAndPunctuation = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0x3000, 0x303F, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var hiragana = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0x3040, 0x309F, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var katakana = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0x30A0, 0x30FF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var kanbun = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0x3130, 0x318F, 1},
|
||||
{0x3190, 0x319F, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var cjkStrokes = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0x31C0, 0x31EF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var katakanaPhoneticExtensions = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0x31F0, 0x31FF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var cjkCompatibility = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0x3300, 0x33FF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var cjkUnifiedIdeographsExtensionA = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0x3400, 0x4DBF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var cjkUnifiedIdeographs = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0x4E00, 0x9FFF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var yiSyllables = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0xA000, 0xA48F, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var yiRadicals = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0xA490, 0xA4CF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var cjkCompatibilityIdeographs = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0xF900, 0xFAFF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var verticalForms = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0xFE10, 0xFE1F, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var cjkCompatibilityForms = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0xFE30, 0xFE4F, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var smallFormVariants = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0xFE50, 0xFE6F, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var halfwidthAndFullwidthForms = &unicode.RangeTable{
|
||||
R16: []unicode.Range16{
|
||||
{0xFF00, 0xFFEF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var kanaSupplement = &unicode.RangeTable{
|
||||
R32: []unicode.Range32{
|
||||
{0x1B000, 0x1B0FF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var kanaExtendedA = &unicode.RangeTable{
|
||||
R32: []unicode.Range32{
|
||||
{0x1B100, 0x1B12F, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var smallKanaExtension = &unicode.RangeTable{
|
||||
R32: []unicode.Range32{
|
||||
{0x1B130, 0x1B16F, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var cjkUnifiedIdeographsExtensionB = &unicode.RangeTable{
|
||||
R32: []unicode.Range32{
|
||||
{0x20000, 0x2A6DF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var cjkUnifiedIdeographsExtensionC = &unicode.RangeTable{
|
||||
R32: []unicode.Range32{
|
||||
{0x2A700, 0x2B73F, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var cjkUnifiedIdeographsExtensionD = &unicode.RangeTable{
|
||||
R32: []unicode.Range32{
|
||||
{0x2B740, 0x2B81F, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var cjkUnifiedIdeographsExtensionE = &unicode.RangeTable{
|
||||
R32: []unicode.Range32{
|
||||
{0x2B820, 0x2CEAF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var cjkUnifiedIdeographsExtensionF = &unicode.RangeTable{
|
||||
R32: []unicode.Range32{
|
||||
{0x2CEB0, 0x2EBEF, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var cjkCompatibilityIdeographsSupplement = &unicode.RangeTable{
|
||||
R32: []unicode.Range32{
|
||||
{0x2F800, 0x2FA1F, 1},
|
||||
},
|
||||
}
|
||||
|
||||
var cjkUnifiedIdeographsExtensionG = &unicode.RangeTable{
|
||||
R32: []unicode.Range32{
|
||||
{0x30000, 0x3134F, 1},
|
||||
},
|
||||
}
|
||||
|
||||
// IsEastAsianWideRune returns trhe if the given rune is an east asian wide character, otherwise false.
|
||||
func IsEastAsianWideRune(r rune) bool {
|
||||
return unicode.Is(unicode.Hiragana, r) ||
|
||||
unicode.Is(unicode.Katakana, r) ||
|
||||
unicode.Is(unicode.Han, r) ||
|
||||
unicode.Is(unicode.Lm, r) ||
|
||||
unicode.Is(unicode.Hangul, r) ||
|
||||
unicode.Is(cjkSymbolsAndPunctuation, r)
|
||||
}
|
||||
|
||||
// IsSpaceDiscardingUnicodeRune returns true if the given rune is space-discarding unicode character, otherwise false.
|
||||
// See https://www.w3.org/TR/2020/WD-css-text-3-20200429/#space-discard-set
|
||||
func IsSpaceDiscardingUnicodeRune(r rune) bool {
|
||||
return unicode.Is(cjkRadicalsSupplement, r) ||
|
||||
unicode.Is(kangxiRadicals, r) ||
|
||||
unicode.Is(ideographicDescriptionCharacters, r) ||
|
||||
unicode.Is(cjkSymbolsAndPunctuation, r) ||
|
||||
unicode.Is(hiragana, r) ||
|
||||
unicode.Is(katakana, r) ||
|
||||
unicode.Is(kanbun, r) ||
|
||||
unicode.Is(cjkStrokes, r) ||
|
||||
unicode.Is(katakanaPhoneticExtensions, r) ||
|
||||
unicode.Is(cjkCompatibility, r) ||
|
||||
unicode.Is(cjkUnifiedIdeographsExtensionA, r) ||
|
||||
unicode.Is(cjkUnifiedIdeographs, r) ||
|
||||
unicode.Is(yiSyllables, r) ||
|
||||
unicode.Is(yiRadicals, r) ||
|
||||
unicode.Is(cjkCompatibilityIdeographs, r) ||
|
||||
unicode.Is(verticalForms, r) ||
|
||||
unicode.Is(cjkCompatibilityForms, r) ||
|
||||
unicode.Is(smallFormVariants, r) ||
|
||||
unicode.Is(halfwidthAndFullwidthForms, r) ||
|
||||
unicode.Is(kanaSupplement, r) ||
|
||||
unicode.Is(kanaExtendedA, r) ||
|
||||
unicode.Is(smallKanaExtension, r) ||
|
||||
unicode.Is(cjkUnifiedIdeographsExtensionB, r) ||
|
||||
unicode.Is(cjkUnifiedIdeographsExtensionC, r) ||
|
||||
unicode.Is(cjkUnifiedIdeographsExtensionD, r) ||
|
||||
unicode.Is(cjkUnifiedIdeographsExtensionE, r) ||
|
||||
unicode.Is(cjkUnifiedIdeographsExtensionF, r) ||
|
||||
unicode.Is(cjkCompatibilityIdeographsSupplement, r) ||
|
||||
unicode.Is(cjkUnifiedIdeographsExtensionG, r)
|
||||
}
|
||||
|
||||
// EastAsianWidth returns the east asian width of the given rune.
|
||||
// See https://www.unicode.org/reports/tr11/tr11-36.html
|
||||
func EastAsianWidth(r rune) string {
|
||||
switch {
|
||||
case r == 0x3000,
|
||||
(0xFF01 <= r && r <= 0xFF60),
|
||||
(0xFFE0 <= r && r <= 0xFFE6):
|
||||
return "F"
|
||||
|
||||
case r == 0x20A9,
|
||||
(0xFF61 <= r && r <= 0xFFBE),
|
||||
(0xFFC2 <= r && r <= 0xFFC7),
|
||||
(0xFFCA <= r && r <= 0xFFCF),
|
||||
(0xFFD2 <= r && r <= 0xFFD7),
|
||||
(0xFFDA <= r && r <= 0xFFDC),
|
||||
(0xFFE8 <= r && r <= 0xFFEE):
|
||||
return "H"
|
||||
|
||||
case (0x1100 <= r && r <= 0x115F),
|
||||
(0x11A3 <= r && r <= 0x11A7),
|
||||
(0x11FA <= r && r <= 0x11FF),
|
||||
(0x2329 <= r && r <= 0x232A),
|
||||
(0x2E80 <= r && r <= 0x2E99),
|
||||
(0x2E9B <= r && r <= 0x2EF3),
|
||||
(0x2F00 <= r && r <= 0x2FD5),
|
||||
(0x2FF0 <= r && r <= 0x2FFB),
|
||||
(0x3001 <= r && r <= 0x303E),
|
||||
(0x3041 <= r && r <= 0x3096),
|
||||
(0x3099 <= r && r <= 0x30FF),
|
||||
(0x3105 <= r && r <= 0x312D),
|
||||
(0x3131 <= r && r <= 0x318E),
|
||||
(0x3190 <= r && r <= 0x31BA),
|
||||
(0x31C0 <= r && r <= 0x31E3),
|
||||
(0x31F0 <= r && r <= 0x321E),
|
||||
(0x3220 <= r && r <= 0x3247),
|
||||
(0x3250 <= r && r <= 0x32FE),
|
||||
(0x3300 <= r && r <= 0x4DBF),
|
||||
(0x4E00 <= r && r <= 0xA48C),
|
||||
(0xA490 <= r && r <= 0xA4C6),
|
||||
(0xA960 <= r && r <= 0xA97C),
|
||||
(0xAC00 <= r && r <= 0xD7A3),
|
||||
(0xD7B0 <= r && r <= 0xD7C6),
|
||||
(0xD7CB <= r && r <= 0xD7FB),
|
||||
(0xF900 <= r && r <= 0xFAFF),
|
||||
(0xFE10 <= r && r <= 0xFE19),
|
||||
(0xFE30 <= r && r <= 0xFE52),
|
||||
(0xFE54 <= r && r <= 0xFE66),
|
||||
(0xFE68 <= r && r <= 0xFE6B),
|
||||
(0x1B000 <= r && r <= 0x1B001),
|
||||
(0x1F200 <= r && r <= 0x1F202),
|
||||
(0x1F210 <= r && r <= 0x1F23A),
|
||||
(0x1F240 <= r && r <= 0x1F248),
|
||||
(0x1F250 <= r && r <= 0x1F251),
|
||||
(0x20000 <= r && r <= 0x2F73F),
|
||||
(0x2B740 <= r && r <= 0x2FFFD),
|
||||
(0x30000 <= r && r <= 0x3FFFD):
|
||||
return "W"
|
||||
|
||||
case (0x0020 <= r && r <= 0x007E),
|
||||
(0x00A2 <= r && r <= 0x00A3),
|
||||
(0x00A5 <= r && r <= 0x00A6),
|
||||
r == 0x00AC,
|
||||
r == 0x00AF,
|
||||
(0x27E6 <= r && r <= 0x27ED),
|
||||
(0x2985 <= r && r <= 0x2986):
|
||||
return "Na"
|
||||
|
||||
case (0x00A1 == r),
|
||||
(0x00A4 == r),
|
||||
(0x00A7 <= r && r <= 0x00A8),
|
||||
(0x00AA == r),
|
||||
(0x00AD <= r && r <= 0x00AE),
|
||||
(0x00B0 <= r && r <= 0x00B4),
|
||||
(0x00B6 <= r && r <= 0x00BA),
|
||||
(0x00BC <= r && r <= 0x00BF),
|
||||
(0x00C6 == r),
|
||||
(0x00D0 == r),
|
||||
(0x00D7 <= r && r <= 0x00D8),
|
||||
(0x00DE <= r && r <= 0x00E1),
|
||||
(0x00E6 == r),
|
||||
(0x00E8 <= r && r <= 0x00EA),
|
||||
(0x00EC <= r && r <= 0x00ED),
|
||||
(0x00F0 == r),
|
||||
(0x00F2 <= r && r <= 0x00F3),
|
||||
(0x00F7 <= r && r <= 0x00FA),
|
||||
(0x00FC == r),
|
||||
(0x00FE == r),
|
||||
(0x0101 == r),
|
||||
(0x0111 == r),
|
||||
(0x0113 == r),
|
||||
(0x011B == r),
|
||||
(0x0126 <= r && r <= 0x0127),
|
||||
(0x012B == r),
|
||||
(0x0131 <= r && r <= 0x0133),
|
||||
(0x0138 == r),
|
||||
(0x013F <= r && r <= 0x0142),
|
||||
(0x0144 == r),
|
||||
(0x0148 <= r && r <= 0x014B),
|
||||
(0x014D == r),
|
||||
(0x0152 <= r && r <= 0x0153),
|
||||
(0x0166 <= r && r <= 0x0167),
|
||||
(0x016B == r),
|
||||
(0x01CE == r),
|
||||
(0x01D0 == r),
|
||||
(0x01D2 == r),
|
||||
(0x01D4 == r),
|
||||
(0x01D6 == r),
|
||||
(0x01D8 == r),
|
||||
(0x01DA == r),
|
||||
(0x01DC == r),
|
||||
(0x0251 == r),
|
||||
(0x0261 == r),
|
||||
(0x02C4 == r),
|
||||
(0x02C7 == r),
|
||||
(0x02C9 <= r && r <= 0x02CB),
|
||||
(0x02CD == r),
|
||||
(0x02D0 == r),
|
||||
(0x02D8 <= r && r <= 0x02DB),
|
||||
(0x02DD == r),
|
||||
(0x02DF == r),
|
||||
(0x0300 <= r && r <= 0x036F),
|
||||
(0x0391 <= r && r <= 0x03A1),
|
||||
(0x03A3 <= r && r <= 0x03A9),
|
||||
(0x03B1 <= r && r <= 0x03C1),
|
||||
(0x03C3 <= r && r <= 0x03C9),
|
||||
(0x0401 == r),
|
||||
(0x0410 <= r && r <= 0x044F),
|
||||
(0x0451 == r),
|
||||
(0x2010 == r),
|
||||
(0x2013 <= r && r <= 0x2016),
|
||||
(0x2018 <= r && r <= 0x2019),
|
||||
(0x201C <= r && r <= 0x201D),
|
||||
(0x2020 <= r && r <= 0x2022),
|
||||
(0x2024 <= r && r <= 0x2027),
|
||||
(0x2030 == r),
|
||||
(0x2032 <= r && r <= 0x2033),
|
||||
(0x2035 == r),
|
||||
(0x203B == r),
|
||||
(0x203E == r),
|
||||
(0x2074 == r),
|
||||
(0x207F == r),
|
||||
(0x2081 <= r && r <= 0x2084),
|
||||
(0x20AC == r),
|
||||
(0x2103 == r),
|
||||
(0x2105 == r),
|
||||
(0x2109 == r),
|
||||
(0x2113 == r),
|
||||
(0x2116 == r),
|
||||
(0x2121 <= r && r <= 0x2122),
|
||||
(0x2126 == r),
|
||||
(0x212B == r),
|
||||
(0x2153 <= r && r <= 0x2154),
|
||||
(0x215B <= r && r <= 0x215E),
|
||||
(0x2160 <= r && r <= 0x216B),
|
||||
(0x2170 <= r && r <= 0x2179),
|
||||
(0x2189 == r),
|
||||
(0x2190 <= r && r <= 0x2199),
|
||||
(0x21B8 <= r && r <= 0x21B9),
|
||||
(0x21D2 == r),
|
||||
(0x21D4 == r),
|
||||
(0x21E7 == r),
|
||||
(0x2200 == r),
|
||||
(0x2202 <= r && r <= 0x2203),
|
||||
(0x2207 <= r && r <= 0x2208),
|
||||
(0x220B == r),
|
||||
(0x220F == r),
|
||||
(0x2211 == r),
|
||||
(0x2215 == r),
|
||||
(0x221A == r),
|
||||
(0x221D <= r && r <= 0x2220),
|
||||
(0x2223 == r),
|
||||
(0x2225 == r),
|
||||
(0x2227 <= r && r <= 0x222C),
|
||||
(0x222E == r),
|
||||
(0x2234 <= r && r <= 0x2237),
|
||||
(0x223C <= r && r <= 0x223D),
|
||||
(0x2248 == r),
|
||||
(0x224C == r),
|
||||
(0x2252 == r),
|
||||
(0x2260 <= r && r <= 0x2261),
|
||||
(0x2264 <= r && r <= 0x2267),
|
||||
(0x226A <= r && r <= 0x226B),
|
||||
(0x226E <= r && r <= 0x226F),
|
||||
(0x2282 <= r && r <= 0x2283),
|
||||
(0x2286 <= r && r <= 0x2287),
|
||||
(0x2295 == r),
|
||||
(0x2299 == r),
|
||||
(0x22A5 == r),
|
||||
(0x22BF == r),
|
||||
(0x2312 == r),
|
||||
(0x2460 <= r && r <= 0x24E9),
|
||||
(0x24EB <= r && r <= 0x254B),
|
||||
(0x2550 <= r && r <= 0x2573),
|
||||
(0x2580 <= r && r <= 0x258F),
|
||||
(0x2592 <= r && r <= 0x2595),
|
||||
(0x25A0 <= r && r <= 0x25A1),
|
||||
(0x25A3 <= r && r <= 0x25A9),
|
||||
(0x25B2 <= r && r <= 0x25B3),
|
||||
(0x25B6 <= r && r <= 0x25B7),
|
||||
(0x25BC <= r && r <= 0x25BD),
|
||||
(0x25C0 <= r && r <= 0x25C1),
|
||||
(0x25C6 <= r && r <= 0x25C8),
|
||||
(0x25CB == r),
|
||||
(0x25CE <= r && r <= 0x25D1),
|
||||
(0x25E2 <= r && r <= 0x25E5),
|
||||
(0x25EF == r),
|
||||
(0x2605 <= r && r <= 0x2606),
|
||||
(0x2609 == r),
|
||||
(0x260E <= r && r <= 0x260F),
|
||||
(0x2614 <= r && r <= 0x2615),
|
||||
(0x261C == r),
|
||||
(0x261E == r),
|
||||
(0x2640 == r),
|
||||
(0x2642 == r),
|
||||
(0x2660 <= r && r <= 0x2661),
|
||||
(0x2663 <= r && r <= 0x2665),
|
||||
(0x2667 <= r && r <= 0x266A),
|
||||
(0x266C <= r && r <= 0x266D),
|
||||
(0x266F == r),
|
||||
(0x269E <= r && r <= 0x269F),
|
||||
(0x26BE <= r && r <= 0x26BF),
|
||||
(0x26C4 <= r && r <= 0x26CD),
|
||||
(0x26CF <= r && r <= 0x26E1),
|
||||
(0x26E3 == r),
|
||||
(0x26E8 <= r && r <= 0x26FF),
|
||||
(0x273D == r),
|
||||
(0x2757 == r),
|
||||
(0x2776 <= r && r <= 0x277F),
|
||||
(0x2B55 <= r && r <= 0x2B59),
|
||||
(0x3248 <= r && r <= 0x324F),
|
||||
(0xE000 <= r && r <= 0xF8FF),
|
||||
(0xFE00 <= r && r <= 0xFE0F),
|
||||
(0xFFFD == r),
|
||||
(0x1F100 <= r && r <= 0x1F10A),
|
||||
(0x1F110 <= r && r <= 0x1F12D),
|
||||
(0x1F130 <= r && r <= 0x1F169),
|
||||
(0x1F170 <= r && r <= 0x1F19A),
|
||||
(0xE0100 <= r && r <= 0xE01EF),
|
||||
(0xF0000 <= r && r <= 0xFFFFD),
|
||||
(0x100000 <= r && r <= 0x10FFFD):
|
||||
return "A"
|
||||
|
||||
default:
|
||||
return "N"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
// +build appengine,js
|
||||
//go:build appengine || js
|
||||
// +build appengine js
|
||||
|
||||
package util
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// +build !appengine,!js
|
||||
//go:build !appengine && !js && !go1.21
|
||||
// +build !appengine,!js,!go1.21
|
||||
|
||||
package util
|
||||
|
||||
|
|
@ -13,8 +14,11 @@ func BytesToReadOnlyString(b []byte) string {
|
|||
}
|
||||
|
||||
// StringToReadOnlyBytes returns bytes converted from given string.
|
||||
func StringToReadOnlyBytes(s string) []byte {
|
||||
func StringToReadOnlyBytes(s string) (bs []byte) {
|
||||
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||
bh := reflect.SliceHeader{Data: sh.Data, Len: sh.Len, Cap: sh.Len}
|
||||
return *(*[]byte)(unsafe.Pointer(&bh))
|
||||
bh := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
|
||||
bh.Data = sh.Data
|
||||
bh.Cap = sh.Len
|
||||
bh.Len = sh.Len
|
||||
return
|
||||
}
|
||||
18
util/util_unsafe_go121.go
Normal file
18
util/util_unsafe_go121.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
//go:build !appengine && !js && go1.21
|
||||
// +build !appengine,!js,go1.21
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// BytesToReadOnlyString returns a string converted from given bytes.
|
||||
func BytesToReadOnlyString(b []byte) string {
|
||||
return unsafe.String(unsafe.SliceData(b), len(b))
|
||||
}
|
||||
|
||||
// StringToReadOnlyBytes returns bytes converted from given string.
|
||||
func StringToReadOnlyBytes(s string) []byte {
|
||||
return unsafe.Slice(unsafe.StringData(s), len(s))
|
||||
}
|
||||
Loading…
Reference in a new issue