├── .github
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── custom.md
│ └── feature_request.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── codeql-analysis.yml
│ └── publish.yml
├── .gitignore
├── .node-version
├── .npmrc
├── .travis.yml
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── SECURITY.md
├── biome.json
├── docs
├── index.html
└── script.js
├── jsr.json
├── node.config.json
├── package-lock.json
├── package.json
├── renovate.json
├── src
├── block
│ ├── CodeBlock.ts
│ ├── Line.ts
│ ├── Pack.ts
│ ├── Row.ts
│ ├── Table.ts
│ ├── Title.ts
│ ├── index.ts
│ └── node
│ │ ├── BlankNode.ts
│ │ ├── CodeNode.ts
│ │ ├── CommandLineNode.ts
│ │ ├── DecorationNode.ts
│ │ ├── ExternalLinkNode.ts
│ │ ├── FormulaNode.ts
│ │ ├── GoogleMapNode.ts
│ │ ├── HashTagNode.ts
│ │ ├── HelpfeelNode.ts
│ │ ├── IconNode.ts
│ │ ├── ImageNode.ts
│ │ ├── InternalLinkNode.ts
│ │ ├── NumberListNode.ts
│ │ ├── PlainNode.ts
│ │ ├── QuoteNode.ts
│ │ ├── StrongIconNode.ts
│ │ ├── StrongImageNode.ts
│ │ ├── StrongNode.ts
│ │ ├── creator.ts
│ │ ├── index.ts
│ │ └── type.ts
├── index.ts
└── parse.ts
├── test
├── codeBlock
│ ├── index.test.ts
│ └── index.test.ts.snapshot
├── line
│ ├── blank.test.ts
│ ├── blank.test.ts.snapshot
│ ├── bullet.test.ts
│ ├── bullet.test.ts.snapshot
│ ├── code.test.ts
│ ├── code.test.ts.snapshot
│ ├── commandLine.test.ts
│ ├── commandLine.test.ts.snapshot
│ ├── decoration.test.ts
│ ├── decoration.test.ts.snapshot
│ ├── formula.test.ts
│ ├── formula.test.ts.snapshot
│ ├── googleMap.test.ts
│ ├── googleMap.test.ts.snapshot
│ ├── hashTag.test.ts
│ ├── hashTag.test.ts.snapshot
│ ├── helpfeel.test.ts
│ ├── helpfeel.test.ts.snapshot
│ ├── icon.test.ts
│ ├── icon.test.ts.snapshot
│ ├── image.test.ts
│ ├── image.test.ts.snapshot
│ ├── index.test.ts
│ ├── index.test.ts.snapshot
│ ├── link.test.ts
│ ├── link.test.ts.snapshot
│ ├── numberList.test.ts
│ ├── numberList.test.ts.snapshot
│ ├── plain.test.ts
│ ├── plain.test.ts.snapshot
│ ├── quote.test.ts
│ ├── quote.test.ts.snapshot
│ ├── strong.test.ts
│ ├── strong.test.ts.snapshot
│ ├── strongIcon.test.ts
│ ├── strongIcon.test.ts.snapshot
│ ├── strongImage.test.ts
│ └── strongImage.test.ts.snapshot
├── page
│ ├── index.test.ts
│ ├── index.test.ts.snapshot
│ └── input.txt
├── table
│ ├── index.test.ts
│ └── index.test.ts.snapshot
└── title
│ └── index.test.ts
├── tsconfig.json
├── tsconfig.lint.json
└── tsdown.config.ts
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at shun1856shun@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | All level contributing is welcome!
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 |
28 | - OS: [e.g. iOS]
29 | - Browser [e.g. chrome, safari]
30 | - Version [e.g. 22]
31 |
32 | **Smartphone (please complete the following information):**
33 |
34 | - Device: [e.g. iPhone6]
35 | - OS: [e.g. iOS8.1]
36 | - Browser [e.g. stock browser, safari]
37 | - Version [e.g. 22]
38 |
39 | **Additional context**
40 | Add any other context about the problem here.
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/custom.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Custom issue template
3 | about: Describe this issue template's purpose here.
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Proposed Changes
2 |
3 | -
4 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [main]
9 | schedule:
10 | - cron: '0 20 * * 0'
11 |
12 | jobs:
13 | analyze:
14 | name: Analyze
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | # Override automatic language detection by changing the below list
21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
22 | language: ['javascript']
23 | # Learn more...
24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
25 |
26 | steps:
27 | - name: Checkout repository
28 | uses: actions/checkout@v4
29 | with:
30 | # We must fetch at least the immediate parents so that if this is
31 | # a pull request then we can checkout the head.
32 | fetch-depth: 2
33 |
34 | # If this run was triggered by a pull request event, then checkout
35 | # the head of the pull request instead of the merge commit.
36 | - run: git checkout HEAD^2
37 | if: ${{ github.event_name == 'pull_request' }}
38 |
39 | # Initializes the CodeQL tools for scanning.
40 | - name: Initialize CodeQL
41 | uses: github/codeql-action/init@v3
42 | with:
43 | languages: ${{ matrix.language }}
44 |
45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
46 | # If this step fails, then you should remove it and run the build manually (see below)
47 | - name: Autobuild
48 | uses: github/codeql-action/autobuild@v3
49 |
50 | # ℹ️ Command-line programs to run using the OS shell.
51 | # 📚 https://git.io/JvXDl
52 |
53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
54 | # and modify them (or add more) to build your code if your project
55 | # uses a compiled language
56 |
57 | #- run: |
58 | # make bootstrap
59 | # make release
60 |
61 | - name: Perform CodeQL Analysis
62 | uses: github/codeql-action/analyze@v3
63 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: "Publish"
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | new_version:
7 | description: ref. https://docs.npmjs.com/cli/commands/npm-version
8 | required: true
9 | type: choice
10 | options:
11 | - major
12 | - minor
13 | - patch
14 |
15 | jobs:
16 | publish:
17 | runs-on: ubuntu-latest
18 | permissions:
19 | contents: write
20 | id-token: write # The OIDC ID token is used for authentication with JSR.
21 | steps:
22 | - uses: actions/checkout@v4.2.2
23 | - uses: actions/setup-node@v4.4.0
24 | with:
25 | registry-url: 'https://registry.npmjs.org'
26 | - name: Publish package
27 | run: |
28 | # Setup Git user
29 | git config user.name "actions-user"
30 | git config user.email "action@github.com"
31 |
32 | # Set package version
33 | npm version $NEW_VERSION
34 | node -p 'JSON.stringify({ ...require("./jsr.json"), version: require("./package.json").version }, undefined, "\t")' > _jsr.json && mv _jsr.json jsr.json
35 | git add jsr.json
36 | git commit --amend --no-edit
37 |
38 | # Install dependencies
39 | npm ci
40 |
41 | # Publish
42 | npm publish --provenance --access public
43 | npm exec --yes -- jsr publish
44 |
45 | # Push version up commit
46 | git push
47 | env:
48 | NEW_VERSION: ${{ inputs.new_version }}
49 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /dist
2 | *.tsbuildinfo
3 |
4 | ### https://raw.github.com/github/gitignore/4a8e0a151becd5ccbb83e4aca6e6c195f3d506fd/Global/macOS.gitignore
5 |
6 | # General
7 | .DS_Store
8 | .AppleDouble
9 | .LSOverride
10 |
11 | # Icon must end with two \r
12 | Icon
13 |
14 |
15 | # Thumbnails
16 | ._*
17 |
18 | # Files that might appear in the root of a volume
19 | .DocumentRevisions-V100
20 | .fseventsd
21 | .Spotlight-V100
22 | .TemporaryItems
23 | .Trashes
24 | .VolumeIcon.icns
25 | .com.apple.timemachine.donotpresent
26 |
27 | # Directories potentially created on remote AFP share
28 | .AppleDB
29 | .AppleDesktop
30 | Network Trash Folder
31 | Temporary Items
32 | .apdisk
33 |
34 |
35 | ### https://raw.github.com/github/gitignore/4a8e0a151becd5ccbb83e4aca6e6c195f3d506fd/Global/Linux.gitignore
36 |
37 | *~
38 |
39 | # temporary files which can be created if a process still has a handle open of a deleted file
40 | .fuse_hidden*
41 |
42 | # KDE directory preferences
43 | .directory
44 |
45 | # Linux trash folder which might appear on any partition or disk
46 | .Trash-*
47 |
48 | # .nfs files are created when an open file is removed but is still being accessed
49 | .nfs*
50 |
51 |
52 | ### https://raw.github.com/github/gitignore/4a8e0a151becd5ccbb83e4aca6e6c195f3d506fd/Global/Windows.gitignore
53 |
54 | # Windows thumbnail cache files
55 | Thumbs.db
56 | ehthumbs.db
57 | ehthumbs_vista.db
58 |
59 | # Dump file
60 | *.stackdump
61 |
62 | # Folder config file
63 | [Dd]esktop.ini
64 |
65 | # Recycle Bin used on file shares
66 | $RECYCLE.BIN/
67 |
68 | # Windows Installer files
69 | *.cab
70 | *.msi
71 | *.msix
72 | *.msm
73 | *.msp
74 |
75 | # Windows shortcuts
76 | *.lnk
77 |
78 |
79 | ### https://raw.github.com/github/gitignore/4a8e0a151becd5ccbb83e4aca6e6c195f3d506fd/Node.gitignore
80 |
81 | # Logs
82 | logs
83 | *.log
84 | npm-debug.log*
85 | yarn-debug.log*
86 | yarn-error.log*
87 |
88 | # Runtime data
89 | pids
90 | *.pid
91 | *.seed
92 | *.pid.lock
93 |
94 | # Directory for instrumented libs generated by jscoverage/JSCover
95 | lib-cov
96 |
97 | # Coverage directory used by tools like istanbul
98 | coverage
99 |
100 | # nyc test coverage
101 | .nyc_output
102 |
103 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
104 | .grunt
105 |
106 | # Bower dependency directory (https://bower.io/)
107 | bower_components
108 |
109 | # node-waf configuration
110 | .lock-wscript
111 |
112 | # Compiled binary addons (https://nodejs.org/api/addons.html)
113 | build/Release
114 |
115 | # Dependency directories
116 | node_modules/
117 | jspm_packages/
118 |
119 | # TypeScript v1 declaration files
120 | typings/
121 |
122 | # Optional npm cache directory
123 | .npm
124 |
125 | # Optional eslint cache
126 | .eslintcache
127 |
128 | # Optional REPL history
129 | .node_repl_history
130 |
131 | # Output of 'npm pack'
132 | *.tgz
133 |
134 | # Yarn Integrity file
135 | .yarn-integrity
136 |
137 | # dotenv environment variables file
138 | .env
139 |
140 | # parcel-bundler cache (https://parceljs.org/)
141 | .cache
142 |
143 | # next.js build output
144 | .next
145 |
146 | # nuxt.js build output
147 | .nuxt
148 |
149 | # vuepress build output
150 | .vuepress/dist
151 |
152 | # Serverless directories
153 | .serverless
154 |
155 | # FuseBox cache
156 | .fusebox/
157 |
158 |
159 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 23.11.1
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | env:
2 | global:
3 | - CC_TEST_REPORTER_ID=8396b692ab81c7e4778a24aa0c8c6c1a2db4cd42a0b4e9ec08aadf64cd977092
4 | language: node_js
5 | node_js:
6 | - 'stable'
7 | - 'lts/*'
8 |
9 | before_script:
10 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
11 | - chmod +x ./cc-test-reporter
12 | - ./cc-test-reporter before-build
13 | script: npm run lint && npm test
14 | after_script:
15 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
16 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["biomejs.biome", "streetsidesoftware.code-spell-checker"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "decos",
4 | "helpfeel",
5 | "gyazo",
6 | "coord",
7 | "progfay",
8 | "scrapbox",
9 | "backquote",
10 | "hoge",
11 | "fuga",
12 | "piyo",
13 | "tsbuildinfo",
14 | "biomejs"
15 | ],
16 | "editor.defaultFormatter": "biomejs.biome",
17 | "editor.formatOnSave": true
18 | }
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 progfay
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Scrapbox Parser
2 |
3 | [](LICENSE)
4 | [](https://www.npmjs.com/package/@progfay/scrapbox-parser)
5 | [](https://codeclimate.com/github/progfay/scrapbox-parser/maintainability)
6 | [](https://codeclimate.com/github/progfay/scrapbox-parser/coverage)
7 |
8 | parse Scrapbox notation to JavaScript Object
9 |
10 | ## Installation
11 |
12 | ```sh
13 | $ npm i @progfay/scrapbox-parser
14 | ```
15 |
16 | Also, you can install `@progfay/scrapbox-parser` via [JSR](https://jsr.io/@progfay/scrapbox-parser).
17 |
18 | ## Usage
19 |
20 | ```js
21 | import { parse } from "@progfay/scrapbox-parser";
22 |
23 | const PROJECT_NAME = "help";
24 | const PAGE_NAME = "syntax";
25 |
26 | fetch(`https://scrapbox.io/api/pages/${PROJECT_NAME}/${PAGE_NAME}/text`)
27 | .then((response) => response.text())
28 | .then((text) => parse(text));
29 | ```
30 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | ------- | ------------------ |
7 | | 7.0.2 | :white_check_mark: |
8 | | < 7.0.1 | :x: |
9 | | 6.0.3 | :white_check_mark: |
10 | | < 6.0.2 | :x: |
11 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
3 | "organizeImports": { "enabled": true },
4 | "files": {
5 | "ignore": ["./dist", "./coverage"]
6 | },
7 | "linter": {
8 | "enabled": true,
9 | "rules": {
10 | "correctness": {
11 | "useImportExtensions": "error"
12 | }
13 | }
14 | },
15 | "formatter": {
16 | "enabled": true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | scrapbox-parser
9 |
85 |
86 |
87 |
88 |
89 |
90 |
94 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/docs/script.js:
--------------------------------------------------------------------------------
1 | import hljs from "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/es/highlight.min.js";
2 | import { parse } from "https://unpkg.com/@progfay/scrapbox-parser";
3 |
4 | document.getElementById("parse-button").addEventListener("click", () => {
5 | const parsedJson = JSON.stringify(
6 | parse(document.getElementById("plain-text").value),
7 | null,
8 | 2,
9 | ).trim();
10 | const a = hljs.highlight(parsedJson, { language: "json" }).value;
11 | console.log(a);
12 | document.querySelector("#parsed-json>.json").innerHTML = a;
13 | });
14 |
--------------------------------------------------------------------------------
/jsr.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@progfay/scrapbox-parser",
3 | "version": "10.0.0",
4 | "exports": "./src/index.ts"
5 | }
6 |
--------------------------------------------------------------------------------
/node.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://nodejs.org/dist/latest/docs/node-config-schema.json",
3 | "nodeOptions": {
4 | "disable-warning": "ExperimentalWarning"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@progfay/scrapbox-parser",
3 | "version": "10.0.0",
4 | "type": "module",
5 | "description": "parse Scrapbox notation to JavaScript Object",
6 | "files": ["dist"],
7 | "types": "./dist/index.d.ts",
8 | "exports": {
9 | ".": "./dist/index.js",
10 | "./package.json": "./package.json"
11 | },
12 | "devEngines": {
13 | "runtime": {
14 | "name": "node",
15 | "version": ">=23.10.0",
16 | "onFail": "error"
17 | }
18 | },
19 | "scripts": {
20 | "prebuild": "node -e 'fs.rmSync(`dist`, {recursive:true, force:true})'",
21 | "build": "tsdown",
22 | "prepare": "npm run build",
23 | "test": "node --experimental-default-config-file --test --experimental-test-coverage",
24 | "test:update": "npm test -- --experimental-test-snapshots",
25 | "lint": "npm run lint:biome && npm run lint:tsc",
26 | "lint:biome": "biome check .",
27 | "lint:tsc": "tsc -p ./tsconfig.lint.json",
28 | "format": "biome check --write ."
29 | },
30 | "repository": {
31 | "type": "git",
32 | "url": "git+https://github.com/progfay/scrapbox-parser.git"
33 | },
34 | "keywords": ["scrapbox", "parser"],
35 | "author": "progfay",
36 | "license": "MIT",
37 | "bugs": {
38 | "url": "https://github.com/progfay/scrapbox-parser/issues"
39 | },
40 | "homepage": "https://github.com/progfay/scrapbox-parser#readme",
41 | "devDependencies": {
42 | "@biomejs/biome": "1.9.4",
43 | "@types/node": "22.15.30",
44 | "tsdown": "0.12.7",
45 | "typescript": "5.8.3"
46 | },
47 | "publishConfig": {
48 | "access": "public"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/block/CodeBlock.ts:
--------------------------------------------------------------------------------
1 | import type { Row } from "./Row.ts";
2 |
3 | export interface CodeBlockPack {
4 | type: "codeBlock";
5 | rows: Row[];
6 | }
7 |
8 | /**
9 | * Scrapbox {@link https://scrapbox.io/help/Syntax#58348ae2651ee500008d67df | code block} type
10 | */
11 | export interface CodeBlock {
12 | indent: number;
13 | type: "codeBlock";
14 | fileName: string;
15 | content: string;
16 | }
17 |
18 | export const convertToCodeBlock = (pack: CodeBlockPack): CodeBlock => {
19 | const {
20 | rows: [head, ...body],
21 | } = pack;
22 | const { indent = 0, text = "" } = head ?? {};
23 | const fileName: string = text.replace(/^\s*code:/, "");
24 |
25 | return {
26 | indent,
27 | type: "codeBlock",
28 | fileName,
29 | content: body
30 | .map((row: Row): string => row.text.substring(indent + 1))
31 | .join("\n"),
32 | };
33 | };
34 |
--------------------------------------------------------------------------------
/src/block/Line.ts:
--------------------------------------------------------------------------------
1 | import { convertToNodes } from "./node/index.ts";
2 |
3 | import type { Row } from "./Row.ts";
4 | import type { Node } from "./node/type.ts";
5 |
6 | export interface LinePack {
7 | type: "line";
8 | rows: [Row];
9 | }
10 |
11 | /**
12 | * Scrapbox line type
13 | */
14 | export interface Line {
15 | indent: number;
16 | type: "line";
17 | nodes: Node[];
18 | }
19 |
20 | export const convertToLine = (pack: LinePack): Line => {
21 | const { indent, text } = pack.rows[0];
22 | return {
23 | indent,
24 | type: "line",
25 | nodes: convertToNodes(text.substring(indent), {
26 | nested: false,
27 | quoted: false,
28 | context: "line",
29 | }),
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/src/block/Pack.ts:
--------------------------------------------------------------------------------
1 | import type { ParserOption } from "../parse.ts";
2 | import type { CodeBlockPack } from "./CodeBlock.ts";
3 | import type { LinePack } from "./Line.ts";
4 | import type { Row } from "./Row.ts";
5 | import type { TablePack } from "./Table.ts";
6 | import type { TitlePack } from "./Title.ts";
7 |
8 | export type Pack = TitlePack | CodeBlockPack | TablePack | LinePack;
9 |
10 | const isChildRowOfPack = (pack: Pack, row: Row): boolean =>
11 | (pack.type === "codeBlock" || pack.type === "table") &&
12 | row.indent > (pack.rows[0]?.indent ?? 0);
13 |
14 | const packing = (packs: Pack[], row: Row): Pack[] => {
15 | const lastPack = packs[packs.length - 1];
16 | if (lastPack !== undefined && isChildRowOfPack(lastPack, row)) {
17 | lastPack.rows.push(row);
18 | return packs;
19 | }
20 |
21 | packs.push({
22 | type: /^\s*code:/.test(row.text)
23 | ? "codeBlock"
24 | : /^\s*table:/.test(row.text)
25 | ? "table"
26 | : "line",
27 | rows: [row],
28 | });
29 |
30 | return packs;
31 | };
32 |
33 | export const packRows = (rows: Row[], opts: ParserOption): Pack[] => {
34 | if (opts.hasTitle ?? true) {
35 | const [title, ...body] = rows;
36 | if (title === undefined) return [];
37 | return [
38 | {
39 | type: "title",
40 | rows: [title],
41 | },
42 | ...body.reduce(packing, []),
43 | ];
44 | }
45 |
46 | return rows.reduce(packing, []);
47 | };
48 |
--------------------------------------------------------------------------------
/src/block/Row.ts:
--------------------------------------------------------------------------------
1 | export interface Row {
2 | indent: number;
3 | text: string;
4 | }
5 |
6 | export const parseToRows = (input: string): Row[] =>
7 | input.split("\n").map((text) => ({
8 | indent: /^\s+/.exec(text)?.[0]?.length ?? 0,
9 | text,
10 | }));
11 |
--------------------------------------------------------------------------------
/src/block/Table.ts:
--------------------------------------------------------------------------------
1 | import { convertToNodes } from "./node/index.ts";
2 |
3 | import type { Row } from "./Row.ts";
4 | import type { Node } from "./node/type.ts";
5 |
6 | export interface TablePack {
7 | type: "table";
8 | rows: Row[];
9 | }
10 |
11 | /**
12 | * Scrapbox {@link https://scrapbox.io/help/Syntax#58795996651ee5000012d4c7 | table} type
13 | */
14 | export interface Table {
15 | indent: number;
16 | type: "table";
17 | fileName: string;
18 | cells: Node[][][];
19 | }
20 |
21 | export const convertToTable = (pack: TablePack): Table => {
22 | const {
23 | rows: [head, ...body],
24 | } = pack;
25 | const { indent = 0, text = "" } = head ?? {};
26 | const fileName = text.replace(/^\s*table:/, "");
27 |
28 | return {
29 | indent,
30 | type: "table",
31 | fileName,
32 | cells: body
33 | .map((row: Row): string => row.text.substring(indent + 1))
34 | .map((text: string): Node[][] =>
35 | text.split("\t").map((block: string): Node[] =>
36 | convertToNodes(block, {
37 | nested: false,
38 | quoted: false,
39 | context: "table",
40 | }),
41 | ),
42 | ),
43 | };
44 | };
45 |
--------------------------------------------------------------------------------
/src/block/Title.ts:
--------------------------------------------------------------------------------
1 | import type { Row } from "./Row.ts";
2 |
3 | export interface TitlePack {
4 | type: "title";
5 | rows: [Row];
6 | }
7 |
8 | /**
9 | * Scrapbox title type
10 | */
11 | export interface Title {
12 | type: "title";
13 | text: string;
14 | }
15 |
16 | export const convertToTitle = (pack: TitlePack): Title => {
17 | return {
18 | type: "title",
19 | text: pack.rows[0].text,
20 | };
21 | };
22 |
--------------------------------------------------------------------------------
/src/block/index.ts:
--------------------------------------------------------------------------------
1 | import { convertToCodeBlock } from "./CodeBlock.ts";
2 | import { convertToLine } from "./Line.ts";
3 | import { convertToTable } from "./Table.ts";
4 | import { convertToTitle } from "./Title.ts";
5 |
6 | import type { CodeBlock } from "./CodeBlock.ts";
7 | import type { Line } from "./Line.ts";
8 | import type { Pack } from "./Pack.ts";
9 | import type { Table } from "./Table.ts";
10 | import type { Title } from "./Title.ts";
11 |
12 | /**
13 | * Scrapbox block type
14 | */
15 | export type Block = Title | CodeBlock | Table | Line;
16 |
17 | export const convertToBlock = (pack: Pack): Block => {
18 | switch (pack.type) {
19 | case "title":
20 | return convertToTitle(pack);
21 |
22 | case "codeBlock":
23 | return convertToCodeBlock(pack);
24 |
25 | case "table":
26 | return convertToTable(pack);
27 |
28 | case "line":
29 | return convertToLine(pack);
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/src/block/node/BlankNode.ts:
--------------------------------------------------------------------------------
1 | import { createPlainNode } from "./PlainNode.ts";
2 | import { createNodeParser } from "./creator.ts";
3 |
4 | import type { NodeCreator } from "./creator.ts";
5 | import type { NodeParser } from "./index.ts";
6 | import type { BlankNode, PlainNode } from "./type.ts";
7 |
8 | const blankRegExp = /\[\s+\]/;
9 |
10 | const createBlankNode: NodeCreator = (raw, opts) =>
11 | opts.context === "table"
12 | ? createPlainNode(raw, opts)
13 | : [
14 | {
15 | type: "blank",
16 | raw,
17 | text: raw.substring(1, raw.length - 1),
18 | },
19 | ];
20 |
21 | export const BlankNodeParser: NodeParser = createNodeParser(createBlankNode, {
22 | parseOnNested: false,
23 | parseOnQuoted: true,
24 | patterns: [blankRegExp],
25 | });
26 |
--------------------------------------------------------------------------------
/src/block/node/CodeNode.ts:
--------------------------------------------------------------------------------
1 | import { createPlainNode } from "./PlainNode.ts";
2 | import { createNodeParser } from "./creator.ts";
3 |
4 | import type { NodeCreator } from "./creator.ts";
5 | import type { NodeParser } from "./index.ts";
6 | import type { CodeNode, PlainNode } from "./type.ts";
7 |
8 | const codeRegExp = /`.*?`/;
9 |
10 | const createCodeNode: NodeCreator = (raw, opts) =>
11 | opts.context === "table"
12 | ? createPlainNode(raw, opts)
13 | : [
14 | {
15 | type: "code",
16 | raw,
17 | text: raw.substring(1, raw.length - 1),
18 | },
19 | ];
20 |
21 | export const CodeNodeParser: NodeParser = createNodeParser(createCodeNode, {
22 | parseOnNested: false,
23 | parseOnQuoted: true,
24 | patterns: [codeRegExp],
25 | });
26 |
--------------------------------------------------------------------------------
/src/block/node/CommandLineNode.ts:
--------------------------------------------------------------------------------
1 | import { createPlainNode } from "./PlainNode.ts";
2 | import { createNodeParser } from "./creator.ts";
3 |
4 | import type { NodeCreator } from "./creator.ts";
5 | import type { NodeParser } from "./index.ts";
6 | import type { CommandLineNode, PlainNode } from "./type.ts";
7 |
8 | const commandLineRegExp = /^[$%] .+$/;
9 |
10 | const createCommandLineNode: NodeCreator = (
11 | raw: string,
12 | opts,
13 | ) => {
14 | if (opts.context === "table") {
15 | return createPlainNode(raw, opts);
16 | }
17 |
18 | const symbol = raw[0] ?? "";
19 | const text = raw.substring(2);
20 |
21 | return [
22 | {
23 | type: "commandLine",
24 | raw,
25 | symbol,
26 | text,
27 | },
28 | ];
29 | };
30 |
31 | export const CommandLineNodeParser: NodeParser = createNodeParser(
32 | createCommandLineNode,
33 | {
34 | parseOnNested: false,
35 | parseOnQuoted: false,
36 | patterns: [commandLineRegExp],
37 | },
38 | );
39 |
--------------------------------------------------------------------------------
/src/block/node/DecorationNode.ts:
--------------------------------------------------------------------------------
1 | import { createPlainNode } from "./PlainNode.ts";
2 | import { createNodeParser } from "./creator.ts";
3 | import { convertToNodes } from "./index.ts";
4 |
5 | import type { NodeCreator } from "./creator.ts";
6 | import type { NodeParser } from "./index.ts";
7 | import type { DecorationNode, PlainNode } from "./type.ts";
8 |
9 | const decorationRegExp = /\[[!"#%&'()*+,\-./{|}<>_~]+ (?:\[[^[\]]+\]|[^\]])+\]/;
10 |
11 | type DecorationChar =
12 | | "*"
13 | | "!"
14 | | '"'
15 | | "#"
16 | | "%"
17 | | "&"
18 | | "'"
19 | | "("
20 | | ")"
21 | | "+"
22 | | ","
23 | | "-"
24 | | "."
25 | | "/"
26 | | "{"
27 | | "|"
28 | | "}"
29 | | "<"
30 | | ">"
31 | | "_"
32 | | "~";
33 |
34 | type AsteriskDecorationChar =
35 | | "*-1"
36 | | "*-2"
37 | | "*-3"
38 | | "*-4"
39 | | "*-5"
40 | | "*-6"
41 | | "*-7"
42 | | "*-8"
43 | | "*-9"
44 | | "*-10";
45 |
46 | /**
47 | * character type of decoration
48 | */
49 | export type Decoration = Exclude | AsteriskDecorationChar;
50 |
51 | const createDecorationNode: NodeCreator = (
52 | raw,
53 | opts,
54 | ) => {
55 | if (opts.context === "table") {
56 | return createPlainNode(raw, opts);
57 | }
58 |
59 | const separatorIndex = raw.indexOf(" ");
60 | const rawDecos = raw.substring(1, separatorIndex);
61 | const text = raw.substring(separatorIndex + 1, raw.length - 1);
62 |
63 | const decoSet = new Set(rawDecos);
64 | if (decoSet.has("*")) {
65 | const asteriskCount = rawDecos.split("*").length - 1;
66 | decoSet.delete("*");
67 | decoSet.add(`*-${Math.min(asteriskCount, 10)}` as AsteriskDecorationChar);
68 | }
69 |
70 | return [
71 | {
72 | type: "decoration",
73 | raw,
74 | rawDecos,
75 | decos: Array.from(decoSet) as Decoration[],
76 | nodes: convertToNodes(text, { ...opts, nested: true }),
77 | },
78 | ];
79 | };
80 |
81 | export const DecorationNodeParser: NodeParser = createNodeParser(
82 | createDecorationNode,
83 | {
84 | parseOnNested: false,
85 | parseOnQuoted: true,
86 | patterns: [decorationRegExp],
87 | },
88 | );
89 |
--------------------------------------------------------------------------------
/src/block/node/ExternalLinkNode.ts:
--------------------------------------------------------------------------------
1 | import { createPlainNode } from "./PlainNode.ts";
2 | import { createNodeParser } from "./creator.ts";
3 |
4 | import type { NodeCreator } from "./creator.ts";
5 | import type { NodeParser } from "./index.ts";
6 | import type { LinkNode, PlainNode } from "./type.ts";
7 |
8 | const hrefFirstUrlRegExp = /\[https?:\/\/[^\s\]]+\s+[^\]]*[^\s]\]/;
9 | const contentFirstUrlRegExp = /\[[^[\]]*[^\s]\s+https?:\/\/[^\s\]]+\]/;
10 | const bracketedUrlRegExp = /\[https?:\/\/[^\s\]]+\]/;
11 | const httpRegExp = /https?:\/\/[^\s]+/;
12 |
13 | const createExternalLinkNode: NodeCreator = (
14 | raw,
15 | opts,
16 | ) => {
17 | if (opts.context === "table") {
18 | return createPlainNode(raw, opts);
19 | }
20 |
21 | const inner =
22 | raw.startsWith("[") && raw.endsWith("]")
23 | ? raw.substring(1, raw.length - 1)
24 | : raw;
25 |
26 | const isHrefFirst = /^https?:\/\/[^\s\]]/.test(inner);
27 | const match = (
28 | isHrefFirst ? /^https?:\/\/[^\s\]]+/ : /https?:\/\/[^\s\]]+$/
29 | ).exec(inner);
30 | if (match?.[0] === undefined) return [];
31 |
32 | const content = isHrefFirst
33 | ? inner.substring(match[0].length)
34 | : inner.substring(0, match.index - 1);
35 |
36 | return [
37 | {
38 | type: "link",
39 | raw,
40 | pathType: "absolute",
41 | href: match[0],
42 | content: content.trim(),
43 | },
44 | ];
45 | };
46 |
47 | export const ExternalLinkNodeParser: NodeParser = createNodeParser(
48 | createExternalLinkNode,
49 | {
50 | parseOnNested: true,
51 | parseOnQuoted: true,
52 | patterns: [
53 | hrefFirstUrlRegExp,
54 | contentFirstUrlRegExp,
55 | bracketedUrlRegExp,
56 | httpRegExp,
57 | ],
58 | },
59 | );
60 |
--------------------------------------------------------------------------------
/src/block/node/FormulaNode.ts:
--------------------------------------------------------------------------------
1 | import { createPlainNode } from "./PlainNode.ts";
2 | import { createNodeParser } from "./creator.ts";
3 |
4 | import type { NodeCreator } from "./creator.ts";
5 | import type { NodeParser } from "./index.ts";
6 | import type { FormulaNode, PlainNode } from "./type.ts";
7 |
8 | const formulaWithTailHalfSpaceRegExp = /\[\$ .+? \]/;
9 | const formulaRegExp = /\[\$ [^\]]+\]/;
10 |
11 | const createFormulaNode: NodeCreator = (raw, opts) =>
12 | opts.context === "table"
13 | ? createPlainNode(raw, opts)
14 | : [
15 | {
16 | type: "formula",
17 | raw,
18 | formula: raw.substring(3, raw.length - (raw.endsWith(" ]") ? 2 : 1)),
19 | },
20 | ];
21 |
22 | export const FormulaNodeParser: NodeParser = createNodeParser(
23 | createFormulaNode,
24 | {
25 | parseOnNested: false,
26 | parseOnQuoted: true,
27 | patterns: [formulaWithTailHalfSpaceRegExp, formulaRegExp],
28 | },
29 | );
30 |
--------------------------------------------------------------------------------
/src/block/node/GoogleMapNode.ts:
--------------------------------------------------------------------------------
1 | import { createPlainNode } from "./PlainNode.ts";
2 | import { createNodeParser } from "./creator.ts";
3 |
4 | import type { NodeCreator } from "./creator.ts";
5 | import type { NodeParser } from "./index.ts";
6 | import type { GoogleMapNode, PlainNode } from "./type.ts";
7 |
8 | const placeFirstGoogleMapRegExp =
9 | /\[([^\]]*[^\s])\s+([NS]\d+(?:\.\d+)?,[EW]\d+(?:\.\d+)?(?:,Z\d+)?)\]/;
10 | const coordFirstGoogleMapRegExp =
11 | /\[([NS]\d+(?:\.\d+)?,[EW]\d+(?:\.\d+)?(?:,Z\d+)?)(?:\s+([^\]]*[^\s]))?\]/;
12 |
13 | interface Coordinate {
14 | latitude: number;
15 | longitude: number;
16 | zoom: number;
17 | }
18 |
19 | const parseCoordinate: (format: string) => Coordinate = (format) => {
20 | const [lat = "", lng = "", z = ""] = format.split(",");
21 | const latitude = Number.parseFloat(lat.replace(/^N/, "").replace(/^S/, "-"));
22 | const longitude = Number.parseFloat(lng.replace(/^E/, "").replace(/^W/, "-"));
23 | const zoom = /^Z\d+$/.test(z) ? Number.parseInt(z.replace(/^Z/, ""), 10) : 14;
24 | return { latitude, longitude, zoom };
25 | };
26 |
27 | const createGoogleMapNode: NodeCreator = (
28 | raw,
29 | opts,
30 | ) => {
31 | if (opts.context === "table") {
32 | return createPlainNode(raw, opts);
33 | }
34 |
35 | const match =
36 | raw.match(placeFirstGoogleMapRegExp) ??
37 | raw.match(coordFirstGoogleMapRegExp);
38 | if (match === null) return [];
39 |
40 | const isCoordFirst = raw.startsWith("[N") || raw.startsWith("[S");
41 | const [, coord = "", place = ""] = isCoordFirst
42 | ? match
43 | : [match[0], match[2], match[1]];
44 | const { latitude, longitude, zoom } = parseCoordinate(coord);
45 |
46 | const url =
47 | place !== ""
48 | ? `https://www.google.com/maps/place/${encodeURIComponent(
49 | place,
50 | )}/@${latitude},${longitude},${zoom}z`
51 | : `https://www.google.com/maps/@${latitude},${longitude},${zoom}z`;
52 |
53 | return [
54 | {
55 | type: "googleMap",
56 | raw,
57 | latitude,
58 | longitude,
59 | zoom,
60 | place,
61 | url,
62 | },
63 | ];
64 | };
65 |
66 | export const GoogleMapNodeParser: NodeParser = createNodeParser(
67 | createGoogleMapNode,
68 | {
69 | parseOnNested: false,
70 | parseOnQuoted: true,
71 | patterns: [placeFirstGoogleMapRegExp, coordFirstGoogleMapRegExp],
72 | },
73 | );
74 |
--------------------------------------------------------------------------------
/src/block/node/HashTagNode.ts:
--------------------------------------------------------------------------------
1 | import { createPlainNode } from "./PlainNode.ts";
2 | import { createNodeParser } from "./creator.ts";
3 |
4 | import type { NodeCreator } from "./creator.ts";
5 | import type { NodeParser } from "./index.ts";
6 | import type { HashTagNode, PlainNode } from "./type.ts";
7 |
8 | const hashTagRegExp = /(?:^|\s)#\S+/;
9 |
10 | const createHashTagNode: NodeCreator = (raw, opts) => {
11 | if (opts.context === "table") {
12 | return createPlainNode(raw, opts);
13 | }
14 |
15 | if (raw.startsWith("#")) {
16 | return [
17 | {
18 | type: "hashTag",
19 | raw,
20 | href: raw.substring(1),
21 | },
22 | ];
23 | }
24 |
25 | const space = raw.substring(0, 1);
26 | const tag = raw.substring(1);
27 |
28 | return [
29 | ...createPlainNode(space, opts),
30 | {
31 | type: "hashTag",
32 | raw: tag,
33 | href: tag.substring(1),
34 | },
35 | ];
36 | };
37 |
38 | export const HashTagNodeParser: NodeParser = createNodeParser(
39 | createHashTagNode,
40 | {
41 | parseOnNested: true,
42 | parseOnQuoted: true,
43 | patterns: [hashTagRegExp],
44 | },
45 | );
46 |
--------------------------------------------------------------------------------
/src/block/node/HelpfeelNode.ts:
--------------------------------------------------------------------------------
1 | import { createPlainNode } from "./PlainNode.ts";
2 | import { createNodeParser } from "./creator.ts";
3 |
4 | import type { NodeCreator } from "./creator.ts";
5 | import type { NodeParser } from "./index.ts";
6 | import type { HelpfeelNode, PlainNode } from "./type.ts";
7 |
8 | const helpfeelRegExp = /^\? .+$/;
9 |
10 | const createHelpfeelNode: NodeCreator = (
11 | raw,
12 | opts,
13 | ) =>
14 | opts.context === "table"
15 | ? createPlainNode(raw, opts)
16 | : [
17 | {
18 | type: "helpfeel",
19 | raw,
20 | text: raw.substring(2),
21 | },
22 | ];
23 |
24 | export const HelpfeelNodeParser: NodeParser = createNodeParser(
25 | createHelpfeelNode,
26 | {
27 | parseOnNested: false,
28 | parseOnQuoted: false,
29 | patterns: [helpfeelRegExp],
30 | },
31 | );
32 |
--------------------------------------------------------------------------------
/src/block/node/IconNode.ts:
--------------------------------------------------------------------------------
1 | import { createNodeParser } from "./creator.ts";
2 | import type { NodeCreator } from "./creator.ts";
3 | import type { NodeParser } from "./index.ts";
4 | import type { IconNode } from "./type.ts";
5 |
6 | const iconRegExp = /\[[^[\]]*\.icon(?:\*[1-9]\d*)?\]/;
7 |
8 | const createIconNode: NodeCreator = (raw) => {
9 | const target = raw.substring(1, raw.length - 1);
10 | const index = target.lastIndexOf(".icon");
11 | const path = target.substring(0, index);
12 | const pathType = path.startsWith("/") ? "root" : "relative";
13 | const numStr = target.substring(index + 5, target.length);
14 | const num = numStr.startsWith("*")
15 | ? Number.parseInt(numStr.substring(1), 10)
16 | : 1;
17 | return new Array(num)
18 | .fill({})
19 | .map(() => ({ path, pathType, type: "icon", raw }));
20 | };
21 |
22 | export const IconNodeParser: NodeParser = createNodeParser(createIconNode, {
23 | parseOnNested: true,
24 | parseOnQuoted: true,
25 | patterns: [iconRegExp],
26 | });
27 |
--------------------------------------------------------------------------------
/src/block/node/ImageNode.ts:
--------------------------------------------------------------------------------
1 | import { createPlainNode } from "./PlainNode.ts";
2 | import { createNodeParser } from "./creator.ts";
3 |
4 | import type { NodeCreator } from "./creator.ts";
5 | import type { NodeParser } from "./index.ts";
6 | import type { ImageNode, PlainNode } from "./type.ts";
7 |
8 | const srcFirstStrongImageRegExp =
9 | /\[https?:\/\/[^\s\]]+\.(?:png|jpe?g|gif|svg|webp)(?:\?[^\]\s]+)?(?:\s+https?:\/\/[^\s\]]+)?\]/i;
10 | const linkFirstStrongImageRegExp =
11 | /\[https?:\/\/[^\s\]]+\s+https?:\/\/[^\s\]]+\.(?:png|jpe?g|gif|svg|webp)(?:\?[^\]\s]+)?\]/i;
12 | const srcFirstStrongGyazoImageRegExp =
13 | /\[https?:\/\/(?:[0-9a-z-]+\.)?gyazo\.com\/[0-9a-f]{32}(?:\/raw)?(?:\s+https?:\/\/[^\s\]]+)?\]/;
14 | const linkFirstStrongGyazoImageRegExp =
15 | /\[https?:\/\/[^\s\]]+\s+https?:\/\/(?:[0-9a-z-]+\.)?gyazo\.com\/[0-9a-f]{32}(?:\/raw)?\]/;
16 |
17 | const isImageUrl = (text: string): boolean =>
18 | /^https?:\/\/[^\s\]]+\.(png|jpe?g|gif|svg|webp)(\?[^\]\s]+)?$/i.test(text) ||
19 | isGyazoImageUrl(text);
20 |
21 | const isGyazoImageUrl = (text: string): boolean =>
22 | /^https?:\/\/([0-9a-z-]\.)?gyazo\.com\/[0-9a-f]{32}(\/raw)?$/.test(text);
23 |
24 | const createImageNode: NodeCreator = (raw, opts) => {
25 | if (opts.context === "table") {
26 | return createPlainNode(raw, opts);
27 | }
28 |
29 | const index = raw.search(/\s/);
30 | const first =
31 | index !== -1 ? raw.substring(1, index) : raw.substring(1, raw.length - 1);
32 | const second =
33 | index !== -1
34 | ? raw.substring(index, raw.length - 1).replace(/^\s+/, "")
35 | : "";
36 | const [src, link] = isImageUrl(second) ? [second, first] : [first, second];
37 |
38 | return [
39 | {
40 | type: "image",
41 | raw,
42 | src: /^https?:\/\/([0-9a-z-]\.)?gyazo\.com\/[0-9a-f]{32}$/.test(src)
43 | ? `${src}/thumb/1000`
44 | : src,
45 | link,
46 | },
47 | ];
48 | };
49 |
50 | export const ImageNodeParser: NodeParser = createNodeParser(createImageNode, {
51 | parseOnNested: true,
52 | parseOnQuoted: true,
53 | patterns: [
54 | srcFirstStrongImageRegExp,
55 | linkFirstStrongImageRegExp,
56 | srcFirstStrongGyazoImageRegExp,
57 | linkFirstStrongGyazoImageRegExp,
58 | ],
59 | });
60 |
--------------------------------------------------------------------------------
/src/block/node/InternalLinkNode.ts:
--------------------------------------------------------------------------------
1 | import { createNodeParser } from "./creator.ts";
2 |
3 | import type { NodeCreator } from "./creator.ts";
4 | import type { NodeParser } from "./index.ts";
5 | import type { LinkNode } from "./type.ts";
6 |
7 | const internalLinkRegExp = /\[\/?[^[\]]+\]/;
8 |
9 | const createInternalLinkNode: NodeCreator = (raw) => {
10 | const href = raw.substring(1, raw.length - 1);
11 | return [
12 | {
13 | type: "link",
14 | raw,
15 | pathType: href.startsWith("/") ? "root" : "relative",
16 | href,
17 | content: "",
18 | },
19 | ];
20 | };
21 |
22 | export const InternalLinkNodeParser: NodeParser = createNodeParser(
23 | createInternalLinkNode,
24 | {
25 | parseOnNested: true,
26 | parseOnQuoted: true,
27 | patterns: [internalLinkRegExp],
28 | },
29 | );
30 |
--------------------------------------------------------------------------------
/src/block/node/NumberListNode.ts:
--------------------------------------------------------------------------------
1 | import { createPlainNode } from "./PlainNode.ts";
2 | import { createNodeParser } from "./creator.ts";
3 | import { type NodeParser, convertToNodes } from "./index.ts";
4 |
5 | import type { NodeCreator } from "./creator.ts";
6 | import type { NumberListNode, PlainNode } from "./type.ts";
7 |
8 | const numberListRegExp = /^[0-9]+\. .*$/;
9 |
10 | const createNumberListNode: NodeCreator = (
11 | raw,
12 | opts,
13 | ) => {
14 | if (opts.context === "table") {
15 | return createPlainNode(raw, opts);
16 | }
17 |
18 | const separatorIndex = raw.indexOf(" ");
19 | const rawNumber = raw.substring(0, separatorIndex - 1);
20 | const number = Number.parseInt(rawNumber, 10);
21 | const text = raw.substring(separatorIndex + 1, raw.length);
22 | return [
23 | {
24 | type: "numberList",
25 | raw,
26 | rawNumber,
27 | number,
28 | nodes: convertToNodes(text, { ...opts, nested: false }),
29 | },
30 | ];
31 | };
32 |
33 | export const NumberListNodeParser: NodeParser = createNodeParser(
34 | createNumberListNode,
35 | {
36 | parseOnNested: false,
37 | parseOnQuoted: false,
38 | patterns: [numberListRegExp],
39 | },
40 | );
41 |
--------------------------------------------------------------------------------
/src/block/node/PlainNode.ts:
--------------------------------------------------------------------------------
1 | import { createNodeParser } from "./creator.ts";
2 |
3 | import type { NodeCreator } from "./creator.ts";
4 | import type { NodeParser } from "./index.ts";
5 | import type { PlainNode } from "./type.ts";
6 |
7 | export const createPlainNode: NodeCreator = (raw) => [
8 | {
9 | type: "plain",
10 | raw,
11 | text: raw,
12 | },
13 | ];
14 |
15 | export const PlainNodeParser: NodeParser = createNodeParser(createPlainNode, {
16 | parseOnNested: true,
17 | parseOnQuoted: true,
18 | patterns: [/^()(.*)()$/],
19 | });
20 |
--------------------------------------------------------------------------------
/src/block/node/QuoteNode.ts:
--------------------------------------------------------------------------------
1 | import { createPlainNode } from "./PlainNode.ts";
2 | import { createNodeParser } from "./creator.ts";
3 | import { type NodeParser, convertToNodes } from "./index.ts";
4 |
5 | import type { NodeCreator } from "./creator.ts";
6 | import type { PlainNode, QuoteNode } from "./type.ts";
7 |
8 | const quoteRegExp = /^>.*$/;
9 |
10 | const createQuoteNode: NodeCreator = (raw, opts) =>
11 | opts.context === "table"
12 | ? createPlainNode(raw, opts)
13 | : [
14 | {
15 | type: "quote",
16 | raw,
17 | nodes: convertToNodes(raw.substring(1), { ...opts, quoted: true }),
18 | },
19 | ];
20 |
21 | export const QuoteNodeParser: NodeParser = createNodeParser(createQuoteNode, {
22 | parseOnNested: false,
23 | parseOnQuoted: false,
24 | patterns: [quoteRegExp],
25 | });
26 |
--------------------------------------------------------------------------------
/src/block/node/StrongIconNode.ts:
--------------------------------------------------------------------------------
1 | import { createPlainNode } from "./PlainNode.ts";
2 | import { type NodeCreator, createNodeParser } from "./creator.ts";
3 | import type { NodeParser } from "./index.ts";
4 | import type { PlainNode, StrongIconNode } from "./type.ts";
5 |
6 | const strongIconRegExp = /\[\[[^[\]]*\.icon(?:\*\d+)?\]\]/;
7 |
8 | const createStrongIconNode: NodeCreator = (
9 | raw,
10 | opts,
11 | ) => {
12 | if (opts.context === "table") return createPlainNode(raw, opts);
13 |
14 | const target = raw.substring(2, raw.length - 2);
15 | const index = target.lastIndexOf(".icon");
16 | const path = target.substring(0, index);
17 | const pathType = path.startsWith("/") ? "root" : "relative";
18 | const numStr = target.substring(index + 5, target.length);
19 | const num = numStr.startsWith("*")
20 | ? Number.parseInt(numStr.substring(1), 10)
21 | : 1;
22 | return new Array(num)
23 | .fill({})
24 | .map(() => ({ path, pathType, type: "strongIcon", raw }));
25 | };
26 |
27 | export const StrongIconNodeParser: NodeParser = createNodeParser(
28 | createStrongIconNode,
29 | {
30 | parseOnNested: false,
31 | parseOnQuoted: true,
32 | patterns: [strongIconRegExp],
33 | },
34 | );
35 |
--------------------------------------------------------------------------------
/src/block/node/StrongImageNode.ts:
--------------------------------------------------------------------------------
1 | import { createPlainNode } from "./PlainNode.ts";
2 | import { createNodeParser } from "./creator.ts";
3 |
4 | import type { NodeCreator } from "./creator.ts";
5 | import type { NodeParser } from "./index.ts";
6 | import type { PlainNode, StrongImageNode } from "./type.ts";
7 |
8 | const strongImageRegExp =
9 | /\[\[https?:\/\/[^\s\]]+\.(?:png|jpe?g|gif|svg|webp)\]\]/i;
10 | const strongGyazoImageRegExp =
11 | /\[\[https?:\/\/(?:[0-9a-z-]+\.)?gyazo\.com\/[0-9a-f]{32}\]\]/;
12 |
13 | const createStrongImageNode: NodeCreator = (
14 | raw,
15 | opts,
16 | ) => {
17 | if (opts.context === "table") {
18 | return createPlainNode(raw, opts);
19 | }
20 |
21 | const src = raw.substring(2, raw.length - 2);
22 | const isGyazoImage =
23 | /^https?:\/\/([0-9a-z-]\.)?gyazo\.com\/[0-9a-f]{32}$/.test(src);
24 | return [
25 | {
26 | type: "strongImage",
27 | raw,
28 | src: isGyazoImage ? `${src}/thumb/1000` : src,
29 | },
30 | ];
31 | };
32 |
33 | export const StrongImageNodeParser: NodeParser = createNodeParser(
34 | createStrongImageNode,
35 | {
36 | parseOnNested: false,
37 | parseOnQuoted: true,
38 | patterns: [strongImageRegExp, strongGyazoImageRegExp],
39 | },
40 | );
41 |
--------------------------------------------------------------------------------
/src/block/node/StrongNode.ts:
--------------------------------------------------------------------------------
1 | import { createPlainNode } from "./PlainNode.ts";
2 | import { createNodeParser } from "./creator.ts";
3 | import { convertToNodes } from "./index.ts";
4 |
5 | import type { NodeCreator } from "./creator.ts";
6 | import type { NodeParser } from "./index.ts";
7 | import type { PlainNode, StrongNode } from "./type.ts";
8 |
9 | const strongRegExp = /\[\[(?:[^[]|\[[^[]).*?\]*\]\]/;
10 |
11 | const createStrongNode: NodeCreator = (raw, opts) =>
12 | opts.context === "table"
13 | ? createPlainNode(raw, opts)
14 | : [
15 | {
16 | type: "strong",
17 | raw,
18 | nodes: convertToNodes(raw.substring(2, raw.length - 2), {
19 | ...opts,
20 | nested: true,
21 | }),
22 | },
23 | ];
24 |
25 | export const StrongNodeParser: NodeParser = createNodeParser(createStrongNode, {
26 | parseOnNested: false,
27 | parseOnQuoted: true,
28 | patterns: [strongRegExp],
29 | });
30 |
--------------------------------------------------------------------------------
/src/block/node/creator.ts:
--------------------------------------------------------------------------------
1 | import { convertToNodes } from "./index.ts";
2 |
3 | import type { NodeParser, NodeParserOption } from "./index.ts";
4 | import type { Node } from "./type.ts";
5 |
6 | export type NodeCreator = (
7 | target: string,
8 | opts: NodeParserOption,
9 | ) => T[];
10 |
11 | type NodeParserCreator = (
12 | nodeCreator: NodeCreator,
13 | opts: { parseOnNested: boolean; parseOnQuoted: boolean; patterns: RegExp[] },
14 | ) => NodeParser;
15 |
16 | export const createNodeParser: NodeParserCreator = (
17 | nodeCreator,
18 | { parseOnNested, parseOnQuoted, patterns },
19 | ) => {
20 | return (text, opts, next) => {
21 | if (!parseOnNested && opts.nested) return next?.() ?? [];
22 | if (!parseOnQuoted && opts.quoted) return next?.() ?? [];
23 |
24 | for (const pattern of patterns) {
25 | const match = pattern.exec(text);
26 | if (match === null) continue;
27 |
28 | const left = text.substring(0, match.index);
29 | const right = text.substring(match.index + (match[0]?.length ?? 0));
30 |
31 | const node = nodeCreator(match[0] ?? "", opts);
32 | return [
33 | ...convertToNodes(left, opts),
34 | ...node,
35 | ...convertToNodes(right, opts),
36 | ];
37 | }
38 |
39 | return next?.() ?? [];
40 | };
41 | };
42 |
--------------------------------------------------------------------------------
/src/block/node/index.ts:
--------------------------------------------------------------------------------
1 | import { BlankNodeParser } from "./BlankNode.ts";
2 | import { CodeNodeParser } from "./CodeNode.ts";
3 | import { CommandLineNodeParser } from "./CommandLineNode.ts";
4 | import { DecorationNodeParser } from "./DecorationNode.ts";
5 | import { ExternalLinkNodeParser } from "./ExternalLinkNode.ts";
6 | import { FormulaNodeParser } from "./FormulaNode.ts";
7 | import { GoogleMapNodeParser } from "./GoogleMapNode.ts";
8 | import { HashTagNodeParser } from "./HashTagNode.ts";
9 | import { HelpfeelNodeParser } from "./HelpfeelNode.ts";
10 | import { IconNodeParser } from "./IconNode.ts";
11 | import { ImageNodeParser } from "./ImageNode.ts";
12 | import { InternalLinkNodeParser } from "./InternalLinkNode.ts";
13 | import { NumberListNodeParser } from "./NumberListNode.ts";
14 | import { PlainNodeParser } from "./PlainNode.ts";
15 | import { QuoteNodeParser } from "./QuoteNode.ts";
16 | import { StrongIconNodeParser } from "./StrongIconNode.ts";
17 | import { StrongImageNodeParser } from "./StrongImageNode.ts";
18 | import { StrongNodeParser } from "./StrongNode.ts";
19 |
20 | import type { Node } from "./type.ts";
21 |
22 | export interface NodeParserOption {
23 | nested: boolean;
24 | quoted: boolean;
25 | context: "line" | "table";
26 | }
27 | export type NextNodeParser = () => Node[];
28 | export type NodeParser = (
29 | text: string,
30 | opts: NodeParserOption,
31 | next?: NextNodeParser,
32 | ) => Node[];
33 |
34 | const FalsyEliminator: NodeParser = (text, _, next) => {
35 | if (text === "") return [];
36 | return next?.() ?? [];
37 | };
38 |
39 | const combineNodeParsers =
40 | (...parsers: NodeParser[]) =>
41 | (text: string, opts: NodeParserOption): Node[] =>
42 | parsers.reduceRight(
43 | (acc: NextNodeParser, parser: NodeParser): NextNodeParser =>
44 | () =>
45 | parser(text, opts, acc),
46 | () => PlainNodeParser(text, opts),
47 | )();
48 |
49 | export const convertToNodes: ReturnType =
50 | combineNodeParsers(
51 | FalsyEliminator,
52 | QuoteNodeParser,
53 | HelpfeelNodeParser,
54 | NumberListNodeParser,
55 | CodeNodeParser,
56 | CommandLineNodeParser,
57 | FormulaNodeParser,
58 | BlankNodeParser,
59 | DecorationNodeParser,
60 | StrongImageNodeParser,
61 | StrongIconNodeParser,
62 | StrongNodeParser,
63 | ImageNodeParser,
64 | ExternalLinkNodeParser,
65 | IconNodeParser,
66 | GoogleMapNodeParser,
67 | InternalLinkNodeParser,
68 | HashTagNodeParser,
69 | );
70 |
--------------------------------------------------------------------------------
/src/block/node/type.ts:
--------------------------------------------------------------------------------
1 | import type { Decoration } from "./DecorationNode.ts";
2 |
3 | interface BaseNode {
4 | raw: string;
5 | }
6 |
7 | /**
8 | * Scrapbox {@link https://scrapbox.io/help/Syntax#58348ae2651ee500008d67d8 | quote node} type
9 | */
10 | export interface QuoteNode extends BaseNode {
11 | type: "quote";
12 | nodes: Node[];
13 | }
14 |
15 | /**
16 | * Scrapbox {@link https://scrapbox.io/help-jp/Helpfeel%E8%A8%98%E6%B3%95 | Helpfeel node} type
17 | */
18 | export interface HelpfeelNode extends BaseNode {
19 | type: "helpfeel";
20 | text: string;
21 | }
22 |
23 | /**
24 | * Scrapbox {@link https://scrapbox.io/help-jp/%E3%81%9D%E3%81%AE%E4%BB%96%E3%81%AE%E6%9B%B8%E3%81%8D%E6%96%B9#5cfa1ea397c291000095c81e | strong image node} type
25 | */
26 | export interface StrongImageNode extends BaseNode {
27 | type: "strongImage";
28 | src: string;
29 | }
30 |
31 | /**
32 | * Scrapbox {@link https://scrapbox.io/help/Icon#5ec273358ee92a000078cafe | strong icon node} type
33 | */
34 | export interface StrongIconNode extends BaseNode {
35 | type: "strongIcon";
36 | pathType: "root" | "relative";
37 | path: string;
38 | }
39 |
40 | /**
41 | * Scrapbox {@link https://scrapbox.io/help/Syntax#58348ae2651ee500008d67cb | strong node} type
42 | */
43 | export interface StrongNode extends BaseNode {
44 | type: "strong";
45 | nodes: Node[];
46 | }
47 |
48 | /**
49 | * Scrapbox {@link https://scrapbox.io/help/Syntax#5e7c7a17651ee50000d77b2e | formula node} type
50 | */
51 | export interface FormulaNode extends BaseNode {
52 | type: "formula";
53 | formula: string;
54 | }
55 |
56 | /**
57 | * Scrapbox {@link https://scrapbox.io/help/Syntax#58348ae2651ee500008d67cc | decoration node} type
58 | */
59 | export interface DecorationNode extends BaseNode {
60 | type: "decoration";
61 | rawDecos: string;
62 | decos: Decoration[];
63 | nodes: Node[];
64 | }
65 |
66 | /**
67 | * Scrapbox {@link https://scrapbox.io/help/Syntax#58348ae2651ee500008d67db | code node} type
68 | */
69 | export interface CodeNode extends BaseNode {
70 | type: "code";
71 | text: string;
72 | }
73 |
74 | /**
75 | * Scrapbox {@link https://scrapbox.io/help/Code_notation#587d557d651ee50000dc693d | command line node} type
76 | */
77 | export interface CommandLineNode extends BaseNode {
78 | type: "commandLine";
79 | symbol: string;
80 | text: string;
81 | }
82 |
83 | /**
84 | * Scrapbox blank node type
85 | */
86 | export interface BlankNode extends BaseNode {
87 | type: "blank";
88 | text: string;
89 | }
90 |
91 | /**
92 | * Scrapbox {@link https://scrapbox.io/help/Syntax#58348ae2651ee500008d67b8 | image node} type
93 | */
94 | export interface ImageNode extends BaseNode {
95 | type: "image";
96 | src: string;
97 | link: string;
98 | }
99 |
100 | /**
101 | * Scrapbox {@link https://scrapbox.io/help/Link | link node} type
102 | */
103 | export interface LinkNode extends BaseNode {
104 | type: "link";
105 | pathType: "absolute" | "root" | "relative";
106 | href: string;
107 | content: string;
108 | }
109 |
110 | /**
111 | * Scrapbox {@link https://scrapbox.io/help-jp/Location%E8%A8%98%E6%B3%95 | Google Map node} type
112 | */
113 | export interface GoogleMapNode extends BaseNode {
114 | type: "googleMap";
115 | latitude: number;
116 | longitude: number;
117 | zoom: number;
118 | place: string;
119 | url: string;
120 | }
121 |
122 | /**
123 | * Scrapbox {@link https://scrapbox.io/help/Syntax#58348ae2651ee500008d67c7 | icon node} type
124 | */
125 | export interface IconNode extends BaseNode {
126 | type: "icon";
127 | pathType: "root" | "relative";
128 | path: string;
129 | }
130 |
131 | /**
132 | * Scrapbox {@link https://scrapbox.io/help/Syntax#58348ae2651ee500008d67d5 | hash tag node} type
133 | */
134 | export interface HashTagNode extends BaseNode {
135 | type: "hashTag";
136 | href: string;
137 | }
138 |
139 | /**
140 | * Scrapbox number list node type
141 | */
142 | export interface NumberListNode extends BaseNode {
143 | type: "numberList";
144 | rawNumber: string;
145 | number: number;
146 | nodes: Node[];
147 | }
148 |
149 | /**
150 | * Scrapbox plain node type
151 | */
152 | export interface PlainNode extends BaseNode {
153 | type: "plain";
154 | text: string;
155 | }
156 |
157 | /**
158 | * Scrapbox node type
159 | */
160 | export type Node =
161 | | QuoteNode
162 | | HelpfeelNode
163 | | StrongImageNode
164 | | StrongIconNode
165 | | StrongNode
166 | | FormulaNode
167 | | DecorationNode
168 | | CodeNode
169 | | CommandLineNode
170 | | BlankNode
171 | | ImageNode
172 | | LinkNode
173 | | GoogleMapNode
174 | | IconNode
175 | | HashTagNode
176 | | NumberListNode
177 | | PlainNode;
178 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { parse, getTitle } from "./parse.ts";
2 | export type { ParserOption, Page } from "./parse.ts";
3 | export type { Block } from "./block/index.ts";
4 | export type { Title } from "./block/Title.ts";
5 | export type { CodeBlock } from "./block/CodeBlock.ts";
6 | export type { Table } from "./block/Table.ts";
7 | export type { Line } from "./block/Line.ts";
8 | export type {
9 | Node,
10 | QuoteNode,
11 | HelpfeelNode,
12 | StrongImageNode,
13 | StrongIconNode,
14 | StrongNode,
15 | FormulaNode,
16 | DecorationNode,
17 | CodeNode,
18 | CommandLineNode,
19 | BlankNode,
20 | ImageNode,
21 | LinkNode,
22 | GoogleMapNode,
23 | IconNode,
24 | HashTagNode,
25 | PlainNode,
26 | } from "./block/node/type.ts";
27 | export type { Decoration } from "./block/node/DecorationNode.ts";
28 |
--------------------------------------------------------------------------------
/src/parse.ts:
--------------------------------------------------------------------------------
1 | import { packRows } from "./block/Pack.ts";
2 | import { parseToRows } from "./block/Row.ts";
3 | import { convertToBlock } from "./block/index.ts";
4 |
5 | import type { Block } from "./block/index.ts";
6 |
7 | /**
8 | * parser option type
9 | */
10 | export interface ParserOption {
11 | /**
12 | * is Scrapbox notation text including title
13 | */
14 | hasTitle?: boolean;
15 | }
16 |
17 | /**
18 | * Scrapbox page type
19 | */
20 | export type Page = Block[];
21 |
22 | /**
23 | * parse Scrapbox notation text into JavaScript Object
24 | * @param input raw Scrapbox notation text
25 | * @param opts parser options
26 | * @returns syntax tree of parsed input
27 | */
28 | export const parse = (input: string, opts?: ParserOption): Page => {
29 | const rows = parseToRows(input);
30 | const packs = packRows(rows, { hasTitle: opts?.hasTitle ?? true });
31 | return packs.map(convertToBlock);
32 | };
33 |
34 | /**
35 | * get title of Scrapbox page
36 | * @param input raw Scrapbox notation text
37 | * @returns title of input Scrapbox page
38 | */
39 | export const getTitle = (input: string): string => {
40 | const match = /^\s*\S.*$/m.exec(input);
41 | return match?.[0]?.trim() ?? "Untitled";
42 | };
43 |
--------------------------------------------------------------------------------
/test/codeBlock/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("Code Block", () => {
5 | it("Simple code block", ({ assert }) => {
6 | assert.snapshot(
7 | parse(
8 | `
9 | code:hello.js
10 | function () {
11 | alert(document.location.href)
12 | console.log("hello")
13 | // You can also write comments!
14 | }
15 | `.trim(),
16 | { hasTitle: false },
17 | ),
18 | );
19 | });
20 |
21 | it("Bulleted code block", ({ assert }) => {
22 | assert.snapshot(
23 | parse(
24 | ` code:hello.js
25 | function () {
26 | alert(document.location.href)
27 | console.log("hello")
28 | // You can also write comments!
29 | }`,
30 | { hasTitle: false },
31 | ),
32 | );
33 | });
34 |
35 | it("Code block with bullet", ({ assert }) => {
36 | assert.snapshot(
37 | parse(
38 | ` Bullet
39 | code:hello.js
40 | function () {
41 | alert(document.location.href)
42 | console.log("hello")
43 | // You can also write comments!
44 | }
45 | Bullet`,
46 | { hasTitle: false },
47 | ),
48 | );
49 | });
50 |
51 | it("Consecutive code blocks", ({ assert }) => {
52 | assert.snapshot(
53 | parse(
54 | `
55 | code:hello.js
56 | function () {
57 | alert(document.location.href)
58 | console.log("hello")
59 | // You can also write comments!
60 | }
61 | code:hello.js
62 | function () {
63 | alert(document.location.href)
64 | console.log("hello")
65 | // You can also write comments!
66 | }
67 | `.trim(),
68 | { hasTitle: false },
69 | ),
70 | );
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/test/codeBlock/index.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`Code Block > Bulleted code block 1`] = `
2 | [
3 | {
4 | "indent": 1,
5 | "type": "codeBlock",
6 | "fileName": "hello.js",
7 | "content": "function () {\\n alert(document.location.href)\\n console.log(\\"hello\\")\\n // You can also write comments!\\n}"
8 | }
9 | ]
10 | `;
11 |
12 | exports[`Code Block > Code block with bullet 1`] = `
13 | [
14 | {
15 | "indent": 1,
16 | "type": "line",
17 | "nodes": [
18 | {
19 | "type": "plain",
20 | "raw": "Bullet",
21 | "text": "Bullet"
22 | }
23 | ]
24 | },
25 | {
26 | "indent": 1,
27 | "type": "codeBlock",
28 | "fileName": "hello.js",
29 | "content": "function () {\\n alert(document.location.href)\\n console.log(\\"hello\\")\\n // You can also write comments!\\n}"
30 | },
31 | {
32 | "indent": 1,
33 | "type": "line",
34 | "nodes": [
35 | {
36 | "type": "plain",
37 | "raw": "Bullet",
38 | "text": "Bullet"
39 | }
40 | ]
41 | }
42 | ]
43 | `;
44 |
45 | exports[`Code Block > Consecutive code blocks 1`] = `
46 | [
47 | {
48 | "indent": 0,
49 | "type": "codeBlock",
50 | "fileName": "hello.js",
51 | "content": "function () {\\n alert(document.location.href)\\n console.log(\\"hello\\")\\n // You can also write comments!\\n}"
52 | },
53 | {
54 | "indent": 0,
55 | "type": "codeBlock",
56 | "fileName": "hello.js",
57 | "content": "function () {\\n alert(document.location.href)\\n console.log(\\"hello\\")\\n // You can also write comments!\\n}"
58 | }
59 | ]
60 | `;
61 |
62 | exports[`Code Block > Simple code block 1`] = `
63 | [
64 | {
65 | "indent": 0,
66 | "type": "codeBlock",
67 | "fileName": "hello.js",
68 | "content": "function () {\\n alert(document.location.href)\\n console.log(\\"hello\\")\\n // You can also write comments!\\n}"
69 | }
70 | ]
71 | `;
72 |
--------------------------------------------------------------------------------
/test/line/blank.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-irregular-whitespace */
2 | import { describe, it } from "node:test";
3 | import { parse } from "../../src/index.ts";
4 |
5 | describe("blank", () => {
6 | it("Simple half-space blank", ({ assert }) => {
7 | assert.snapshot(parse("[ ]", { hasTitle: false }));
8 | });
9 |
10 | it("Simple double-byte space blank", ({ assert }) => {
11 | assert.snapshot(parse("[ ]", { hasTitle: false }));
12 | });
13 |
14 | it("Simple tab blank", ({ assert }) => {
15 | assert.snapshot(parse("[\t]", { hasTitle: false }));
16 | });
17 |
18 | it("Multi char blank", ({ assert }) => {
19 | assert.snapshot(parse("[ \t \t ]", { hasTitle: false }));
20 | });
21 |
22 | it("Blank in the sentence", ({ assert }) => {
23 | assert.snapshot(
24 | parse("sentence[ ]sentence", {
25 | hasTitle: false,
26 | }),
27 | );
28 | });
29 |
30 | it("[] is not blank", ({ assert }) => {
31 | assert.snapshot(parse("[]", { hasTitle: false }));
32 | });
33 |
34 | it("Blank in the [*** ]", ({ assert }) => {
35 | assert.snapshot(parse("[*** [ ]]", { hasTitle: false }));
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/test/line/blank.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`blank > Blank in the [*** ] 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "plain",
9 | "raw": "[*** ",
10 | "text": "[*** "
11 | },
12 | {
13 | "type": "blank",
14 | "raw": "[ ]",
15 | "text": " "
16 | },
17 | {
18 | "type": "plain",
19 | "raw": "]",
20 | "text": "]"
21 | }
22 | ]
23 | }
24 | ]
25 | `;
26 |
27 | exports[`blank > Blank in the sentence 1`] = `
28 | [
29 | {
30 | "indent": 0,
31 | "type": "line",
32 | "nodes": [
33 | {
34 | "type": "plain",
35 | "raw": "sentence",
36 | "text": "sentence"
37 | },
38 | {
39 | "type": "blank",
40 | "raw": "[ ]",
41 | "text": " "
42 | },
43 | {
44 | "type": "plain",
45 | "raw": "sentence",
46 | "text": "sentence"
47 | }
48 | ]
49 | }
50 | ]
51 | `;
52 |
53 | exports[`blank > Multi char blank 1`] = `
54 | [
55 | {
56 | "indent": 0,
57 | "type": "line",
58 | "nodes": [
59 | {
60 | "type": "blank",
61 | "raw": "[ \\t \\t ]",
62 | "text": " \\t \\t "
63 | }
64 | ]
65 | }
66 | ]
67 | `;
68 |
69 | exports[`blank > Simple double-byte space blank 1`] = `
70 | [
71 | {
72 | "indent": 0,
73 | "type": "line",
74 | "nodes": [
75 | {
76 | "type": "blank",
77 | "raw": "[ ]",
78 | "text": " "
79 | }
80 | ]
81 | }
82 | ]
83 | `;
84 |
85 | exports[`blank > Simple half-space blank 1`] = `
86 | [
87 | {
88 | "indent": 0,
89 | "type": "line",
90 | "nodes": [
91 | {
92 | "type": "blank",
93 | "raw": "[ ]",
94 | "text": " "
95 | }
96 | ]
97 | }
98 | ]
99 | `;
100 |
101 | exports[`blank > Simple tab blank 1`] = `
102 | [
103 | {
104 | "indent": 0,
105 | "type": "line",
106 | "nodes": [
107 | {
108 | "type": "blank",
109 | "raw": "[\\t]",
110 | "text": "\\t"
111 | }
112 | ]
113 | }
114 | ]
115 | `;
116 |
117 | exports[`blank > [] is not blank 1`] = `
118 | [
119 | {
120 | "indent": 0,
121 | "type": "line",
122 | "nodes": [
123 | {
124 | "type": "plain",
125 | "raw": "[]",
126 | "text": "[]"
127 | }
128 | ]
129 | }
130 | ]
131 | `;
132 |
--------------------------------------------------------------------------------
/test/line/bullet.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("bullet", () => {
5 | it("Single-byte space indent", ({ assert }) => {
6 | assert.snapshot(
7 | parse(" Single-byte space", {
8 | hasTitle: false,
9 | }),
10 | );
11 | });
12 |
13 | it("Double-byte space indent", ({ assert }) => {
14 | assert.snapshot(
15 | parse(" Double-byte space", {
16 | hasTitle: false,
17 | }),
18 | );
19 | });
20 |
21 | it("Tab indent", ({ assert }) => {
22 | // eslint-disable-next-line no-tabs
23 | assert.snapshot(parse(" Tab", { hasTitle: false }));
24 | });
25 |
26 | it("Multi lines bullet", ({ assert }) => {
27 | assert.snapshot(
28 | parse(
29 | `
30 | no bullet (indent: 0)
31 | first bullet (indent: 1)
32 | second bullet (indent: 2)
33 | third bullet (indent: 3)
34 | `.trim(),
35 | { hasTitle: false },
36 | ),
37 | );
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/test/line/bullet.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`bullet > Double-byte space indent 1`] = `
2 | [
3 | {
4 | "indent": 1,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "plain",
9 | "raw": "Double-byte space",
10 | "text": "Double-byte space"
11 | }
12 | ]
13 | }
14 | ]
15 | `;
16 |
17 | exports[`bullet > Multi lines bullet 1`] = `
18 | [
19 | {
20 | "indent": 0,
21 | "type": "line",
22 | "nodes": [
23 | {
24 | "type": "plain",
25 | "raw": "no bullet (indent: 0)",
26 | "text": "no bullet (indent: 0)"
27 | }
28 | ]
29 | },
30 | {
31 | "indent": 1,
32 | "type": "line",
33 | "nodes": [
34 | {
35 | "type": "plain",
36 | "raw": "first bullet (indent: 1)",
37 | "text": "first bullet (indent: 1)"
38 | }
39 | ]
40 | },
41 | {
42 | "indent": 2,
43 | "type": "line",
44 | "nodes": [
45 | {
46 | "type": "plain",
47 | "raw": "second bullet (indent: 2)",
48 | "text": "second bullet (indent: 2)"
49 | }
50 | ]
51 | },
52 | {
53 | "indent": 3,
54 | "type": "line",
55 | "nodes": [
56 | {
57 | "type": "plain",
58 | "raw": "third bullet (indent: 3)",
59 | "text": "third bullet (indent: 3)"
60 | }
61 | ]
62 | }
63 | ]
64 | `;
65 |
66 | exports[`bullet > Single-byte space indent 1`] = `
67 | [
68 | {
69 | "indent": 1,
70 | "type": "line",
71 | "nodes": [
72 | {
73 | "type": "plain",
74 | "raw": "Single-byte space",
75 | "text": "Single-byte space"
76 | }
77 | ]
78 | }
79 | ]
80 | `;
81 |
82 | exports[`bullet > Tab indent 1`] = `
83 | [
84 | {
85 | "indent": 1,
86 | "type": "line",
87 | "nodes": [
88 | {
89 | "type": "plain",
90 | "raw": "Tab",
91 | "text": "Tab"
92 | }
93 | ]
94 | }
95 | ]
96 | `;
97 |
--------------------------------------------------------------------------------
/test/line/code.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("code", () => {
5 | it("Simple code with backquote", ({ assert }) => {
6 | assert.snapshot(parse("`Simple code`", { hasTitle: false }));
7 | });
8 |
9 | it("Empty code with backquote", ({ assert }) => {
10 | assert.snapshot(parse("``", { hasTitle: false }));
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/test/line/code.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`code > Empty code with backquote 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "code",
9 | "raw": "\`\`",
10 | "text": ""
11 | }
12 | ]
13 | }
14 | ]
15 | `;
16 |
17 | exports[`code > Simple code with backquote 1`] = `
18 | [
19 | {
20 | "indent": 0,
21 | "type": "line",
22 | "nodes": [
23 | {
24 | "type": "code",
25 | "raw": "\`Simple code\`",
26 | "text": "Simple code"
27 | }
28 | ]
29 | }
30 | ]
31 | `;
32 |
--------------------------------------------------------------------------------
/test/line/commandLine.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("commandLine", () => {
5 | it("Simple command with $", ({ assert }) => {
6 | assert.snapshot(parse("$ command", { hasTitle: false }));
7 | });
8 |
9 | it("Simple command with %", ({ assert }) => {
10 | assert.snapshot(parse("% command", { hasTitle: false }));
11 | });
12 |
13 | it("`$` is not command", ({ assert }) => {
14 | assert.snapshot(parse("$", { hasTitle: false }));
15 | });
16 |
17 | it("`$ ` is not command", ({ assert }) => {
18 | assert.snapshot(parse("$ ", { hasTitle: false }));
19 | });
20 |
21 | it("`$s` is not command", ({ assert }) => {
22 | assert.snapshot(parse("$not command", { hasTitle: false }));
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/test/line/commandLine.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`commandLine > Simple command with $ 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "commandLine",
9 | "raw": "$ command",
10 | "symbol": "$",
11 | "text": "command"
12 | }
13 | ]
14 | }
15 | ]
16 | `;
17 |
18 | exports[`commandLine > Simple command with % 1`] = `
19 | [
20 | {
21 | "indent": 0,
22 | "type": "line",
23 | "nodes": [
24 | {
25 | "type": "commandLine",
26 | "raw": "% command",
27 | "symbol": "%",
28 | "text": "command"
29 | }
30 | ]
31 | }
32 | ]
33 | `;
34 |
35 | exports[`commandLine > \`$ \` is not command 1`] = `
36 | [
37 | {
38 | "indent": 0,
39 | "type": "line",
40 | "nodes": [
41 | {
42 | "type": "plain",
43 | "raw": "$ ",
44 | "text": "$ "
45 | }
46 | ]
47 | }
48 | ]
49 | `;
50 |
51 | exports[`commandLine > \`$\` is not command 1`] = `
52 | [
53 | {
54 | "indent": 0,
55 | "type": "line",
56 | "nodes": [
57 | {
58 | "type": "plain",
59 | "raw": "$",
60 | "text": "$"
61 | }
62 | ]
63 | }
64 | ]
65 | `;
66 |
67 | exports[`commandLine > \`$s\` is not command 1`] = `
68 | [
69 | {
70 | "indent": 0,
71 | "type": "line",
72 | "nodes": [
73 | {
74 | "type": "plain",
75 | "raw": "$not command",
76 | "text": "$not command"
77 | }
78 | ]
79 | }
80 | ]
81 | `;
82 |
--------------------------------------------------------------------------------
/test/line/decoration.test.ts:
--------------------------------------------------------------------------------
1 | import { deepStrictEqual } from "node:assert/strict";
2 | import { describe, it } from "node:test";
3 | import {
4 | type Decoration,
5 | type DecorationNode,
6 | type Line,
7 | parse,
8 | } from "../../src/index.ts";
9 |
10 | describe("decoration", () => {
11 | it("Simple decoration", ({ assert }) => {
12 | assert.snapshot(
13 | parse(
14 | `
15 | [* deco]
16 | [** deco]
17 | [*** deco]
18 | [**** deco]
19 | [***** deco]
20 | [****** deco]
21 | [******* deco]
22 | [******** deco]
23 | [********* deco]
24 | [********** deco]
25 | [! deco]
26 | [" deco]
27 | [# deco]
28 | [% deco]
29 | [& deco]
30 | [' deco]
31 | [( deco]
32 | [) deco]
33 | [+ deco]
34 | [, deco]
35 | [- deco]
36 | [. deco]
37 | [/ deco]
38 | [{ deco]
39 | [| deco]
40 | [} deco]
41 | [< deco]
42 | [> deco]
43 | [_ deco]
44 | [~ deco]
45 | `.trim(),
46 | { hasTitle: false },
47 | ),
48 | );
49 | });
50 |
51 | it("All decoration", () => {
52 | const input = "[**********!\"#%&'()*+,-./{|}<>_~ decos]";
53 | const blocks = parse(input, { hasTitle: false });
54 | const received = ((blocks[0] as Line).nodes[0] as DecorationNode).decos;
55 | const decos: Decoration[] = [
56 | "*-10",
57 | "!",
58 | '"',
59 | "#",
60 | "%",
61 | "&",
62 | "'",
63 | "(",
64 | ")",
65 | "+",
66 | ",",
67 | "-",
68 | ".",
69 | "/",
70 | "{",
71 | "|",
72 | "}",
73 | "<",
74 | ">",
75 | "_",
76 | "~",
77 | ];
78 | deepStrictEqual(received.sort(), decos.sort());
79 | });
80 |
81 | it("Decoration * overflow", ({ assert }) => {
82 | assert.snapshot(parse("[*********** 11*]", { hasTitle: false }));
83 | });
84 |
85 | it("Decoration similar with externalLink", ({ assert }) => {
86 | assert.snapshot(
87 | parse("[* hoge https://example.com]", {
88 | hasTitle: false,
89 | }),
90 | );
91 | });
92 |
93 | it("Decoration with hashTag", ({ assert }) => {
94 | assert.snapshot(
95 | parse("[* #tag]", {
96 | hasTitle: false,
97 | }),
98 | );
99 | });
100 |
101 | it("Decoration with many [", ({ assert }) => {
102 | assert.snapshot(parse("[! [[[[[[a]", { hasTitle: false }));
103 | });
104 |
105 | it("Decoration with many [ and link", ({ assert }) => {
106 | assert.snapshot(parse("[! [[[[[[a]]", { hasTitle: false }));
107 | });
108 |
109 | it("Decoration with strong notation (it's just link)", ({ assert }) => {
110 | assert.snapshot(parse("[* [[link]]]", { hasTitle: false }));
111 | });
112 |
113 | it("Decoration with icon notation", ({ assert }) => {
114 | assert.snapshot(
115 | parse("[* [progfay.icon]]", {
116 | hasTitle: false,
117 | }),
118 | );
119 | });
120 |
121 | it("Decoration with strong icon notation (it's just icon, not strong)", ({
122 | assert,
123 | }) => {
124 | assert.snapshot(parse("[* [[progfay.icon]]]", { hasTitle: false }));
125 | });
126 |
127 | it("Decoration with strong image notation (it's just image, not strong)", ({
128 | assert,
129 | }) => {
130 | assert.snapshot(
131 | parse("[* [[https://example.com/image.png]]]", {
132 | hasTitle: false,
133 | }),
134 | );
135 | });
136 | });
137 |
--------------------------------------------------------------------------------
/test/line/decoration.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`decoration > Decoration * overflow 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "decoration",
9 | "raw": "[*********** 11*]",
10 | "rawDecos": "***********",
11 | "decos": [
12 | "*-10"
13 | ],
14 | "nodes": [
15 | {
16 | "type": "plain",
17 | "raw": "11*",
18 | "text": "11*"
19 | }
20 | ]
21 | }
22 | ]
23 | }
24 | ]
25 | `;
26 |
27 | exports[`decoration > Decoration similar with externalLink 1`] = `
28 | [
29 | {
30 | "indent": 0,
31 | "type": "line",
32 | "nodes": [
33 | {
34 | "type": "decoration",
35 | "raw": "[* hoge https://example.com]",
36 | "rawDecos": "*",
37 | "decos": [
38 | "*-1"
39 | ],
40 | "nodes": [
41 | {
42 | "type": "plain",
43 | "raw": "hoge ",
44 | "text": "hoge "
45 | },
46 | {
47 | "type": "link",
48 | "raw": "https://example.com",
49 | "pathType": "absolute",
50 | "href": "https://example.com",
51 | "content": ""
52 | }
53 | ]
54 | }
55 | ]
56 | }
57 | ]
58 | `;
59 |
60 | exports[`decoration > Decoration with hashTag 1`] = `
61 | [
62 | {
63 | "indent": 0,
64 | "type": "line",
65 | "nodes": [
66 | {
67 | "type": "decoration",
68 | "raw": "[* #tag]",
69 | "rawDecos": "*",
70 | "decos": [
71 | "*-1"
72 | ],
73 | "nodes": [
74 | {
75 | "type": "hashTag",
76 | "raw": "#tag",
77 | "href": "tag"
78 | }
79 | ]
80 | }
81 | ]
82 | }
83 | ]
84 | `;
85 |
86 | exports[`decoration > Decoration with icon notation 1`] = `
87 | [
88 | {
89 | "indent": 0,
90 | "type": "line",
91 | "nodes": [
92 | {
93 | "type": "decoration",
94 | "raw": "[* [progfay.icon]]",
95 | "rawDecos": "*",
96 | "decos": [
97 | "*-1"
98 | ],
99 | "nodes": [
100 | {
101 | "path": "progfay",
102 | "pathType": "relative",
103 | "type": "icon",
104 | "raw": "[progfay.icon]"
105 | }
106 | ]
107 | }
108 | ]
109 | }
110 | ]
111 | `;
112 |
113 | exports[`decoration > Decoration with many [ 1`] = `
114 | [
115 | {
116 | "indent": 0,
117 | "type": "line",
118 | "nodes": [
119 | {
120 | "type": "decoration",
121 | "raw": "[! [[[[[[a]",
122 | "rawDecos": "!",
123 | "decos": [
124 | "!"
125 | ],
126 | "nodes": [
127 | {
128 | "type": "plain",
129 | "raw": "[[[[[[a",
130 | "text": "[[[[[[a"
131 | }
132 | ]
133 | }
134 | ]
135 | }
136 | ]
137 | `;
138 |
139 | exports[`decoration > Decoration with many [ and link 1`] = `
140 | [
141 | {
142 | "indent": 0,
143 | "type": "line",
144 | "nodes": [
145 | {
146 | "type": "decoration",
147 | "raw": "[! [[[[[[a]]",
148 | "rawDecos": "!",
149 | "decos": [
150 | "!"
151 | ],
152 | "nodes": [
153 | {
154 | "type": "plain",
155 | "raw": "[[[[[",
156 | "text": "[[[[["
157 | },
158 | {
159 | "type": "link",
160 | "raw": "[a]",
161 | "pathType": "relative",
162 | "href": "a",
163 | "content": ""
164 | }
165 | ]
166 | }
167 | ]
168 | }
169 | ]
170 | `;
171 |
172 | exports[`decoration > Decoration with strong icon notation (it's just icon, not strong) 1`] = `
173 | [
174 | {
175 | "indent": 0,
176 | "type": "line",
177 | "nodes": [
178 | {
179 | "type": "decoration",
180 | "raw": "[* [[progfay.icon]]",
181 | "rawDecos": "*",
182 | "decos": [
183 | "*-1"
184 | ],
185 | "nodes": [
186 | {
187 | "type": "plain",
188 | "raw": "[",
189 | "text": "["
190 | },
191 | {
192 | "path": "progfay",
193 | "pathType": "relative",
194 | "type": "icon",
195 | "raw": "[progfay.icon]"
196 | }
197 | ]
198 | },
199 | {
200 | "type": "plain",
201 | "raw": "]",
202 | "text": "]"
203 | }
204 | ]
205 | }
206 | ]
207 | `;
208 |
209 | exports[`decoration > Decoration with strong image notation (it's just image, not strong) 1`] = `
210 | [
211 | {
212 | "indent": 0,
213 | "type": "line",
214 | "nodes": [
215 | {
216 | "type": "decoration",
217 | "raw": "[* [[https://example.com/image.png]]",
218 | "rawDecos": "*",
219 | "decos": [
220 | "*-1"
221 | ],
222 | "nodes": [
223 | {
224 | "type": "plain",
225 | "raw": "[",
226 | "text": "["
227 | },
228 | {
229 | "type": "image",
230 | "raw": "[https://example.com/image.png]",
231 | "src": "https://example.com/image.png",
232 | "link": ""
233 | }
234 | ]
235 | },
236 | {
237 | "type": "plain",
238 | "raw": "]",
239 | "text": "]"
240 | }
241 | ]
242 | }
243 | ]
244 | `;
245 |
246 | exports[`decoration > Decoration with strong notation (it's just link) 1`] = `
247 | [
248 | {
249 | "indent": 0,
250 | "type": "line",
251 | "nodes": [
252 | {
253 | "type": "decoration",
254 | "raw": "[* [[link]]",
255 | "rawDecos": "*",
256 | "decos": [
257 | "*-1"
258 | ],
259 | "nodes": [
260 | {
261 | "type": "plain",
262 | "raw": "[",
263 | "text": "["
264 | },
265 | {
266 | "type": "link",
267 | "raw": "[link]",
268 | "pathType": "relative",
269 | "href": "link",
270 | "content": ""
271 | }
272 | ]
273 | },
274 | {
275 | "type": "plain",
276 | "raw": "]",
277 | "text": "]"
278 | }
279 | ]
280 | }
281 | ]
282 | `;
283 |
284 | exports[`decoration > Simple decoration 1`] = `
285 | [
286 | {
287 | "indent": 0,
288 | "type": "line",
289 | "nodes": [
290 | {
291 | "type": "decoration",
292 | "raw": "[* deco]",
293 | "rawDecos": "*",
294 | "decos": [
295 | "*-1"
296 | ],
297 | "nodes": [
298 | {
299 | "type": "plain",
300 | "raw": "deco",
301 | "text": "deco"
302 | }
303 | ]
304 | }
305 | ]
306 | },
307 | {
308 | "indent": 0,
309 | "type": "line",
310 | "nodes": [
311 | {
312 | "type": "decoration",
313 | "raw": "[** deco]",
314 | "rawDecos": "**",
315 | "decos": [
316 | "*-2"
317 | ],
318 | "nodes": [
319 | {
320 | "type": "plain",
321 | "raw": "deco",
322 | "text": "deco"
323 | }
324 | ]
325 | }
326 | ]
327 | },
328 | {
329 | "indent": 0,
330 | "type": "line",
331 | "nodes": [
332 | {
333 | "type": "decoration",
334 | "raw": "[*** deco]",
335 | "rawDecos": "***",
336 | "decos": [
337 | "*-3"
338 | ],
339 | "nodes": [
340 | {
341 | "type": "plain",
342 | "raw": "deco",
343 | "text": "deco"
344 | }
345 | ]
346 | }
347 | ]
348 | },
349 | {
350 | "indent": 0,
351 | "type": "line",
352 | "nodes": [
353 | {
354 | "type": "decoration",
355 | "raw": "[**** deco]",
356 | "rawDecos": "****",
357 | "decos": [
358 | "*-4"
359 | ],
360 | "nodes": [
361 | {
362 | "type": "plain",
363 | "raw": "deco",
364 | "text": "deco"
365 | }
366 | ]
367 | }
368 | ]
369 | },
370 | {
371 | "indent": 0,
372 | "type": "line",
373 | "nodes": [
374 | {
375 | "type": "decoration",
376 | "raw": "[***** deco]",
377 | "rawDecos": "*****",
378 | "decos": [
379 | "*-5"
380 | ],
381 | "nodes": [
382 | {
383 | "type": "plain",
384 | "raw": "deco",
385 | "text": "deco"
386 | }
387 | ]
388 | }
389 | ]
390 | },
391 | {
392 | "indent": 0,
393 | "type": "line",
394 | "nodes": [
395 | {
396 | "type": "decoration",
397 | "raw": "[****** deco]",
398 | "rawDecos": "******",
399 | "decos": [
400 | "*-6"
401 | ],
402 | "nodes": [
403 | {
404 | "type": "plain",
405 | "raw": "deco",
406 | "text": "deco"
407 | }
408 | ]
409 | }
410 | ]
411 | },
412 | {
413 | "indent": 0,
414 | "type": "line",
415 | "nodes": [
416 | {
417 | "type": "decoration",
418 | "raw": "[******* deco]",
419 | "rawDecos": "*******",
420 | "decos": [
421 | "*-7"
422 | ],
423 | "nodes": [
424 | {
425 | "type": "plain",
426 | "raw": "deco",
427 | "text": "deco"
428 | }
429 | ]
430 | }
431 | ]
432 | },
433 | {
434 | "indent": 0,
435 | "type": "line",
436 | "nodes": [
437 | {
438 | "type": "decoration",
439 | "raw": "[******** deco]",
440 | "rawDecos": "********",
441 | "decos": [
442 | "*-8"
443 | ],
444 | "nodes": [
445 | {
446 | "type": "plain",
447 | "raw": "deco",
448 | "text": "deco"
449 | }
450 | ]
451 | }
452 | ]
453 | },
454 | {
455 | "indent": 0,
456 | "type": "line",
457 | "nodes": [
458 | {
459 | "type": "decoration",
460 | "raw": "[********* deco]",
461 | "rawDecos": "*********",
462 | "decos": [
463 | "*-9"
464 | ],
465 | "nodes": [
466 | {
467 | "type": "plain",
468 | "raw": "deco",
469 | "text": "deco"
470 | }
471 | ]
472 | }
473 | ]
474 | },
475 | {
476 | "indent": 0,
477 | "type": "line",
478 | "nodes": [
479 | {
480 | "type": "decoration",
481 | "raw": "[********** deco]",
482 | "rawDecos": "**********",
483 | "decos": [
484 | "*-10"
485 | ],
486 | "nodes": [
487 | {
488 | "type": "plain",
489 | "raw": "deco",
490 | "text": "deco"
491 | }
492 | ]
493 | }
494 | ]
495 | },
496 | {
497 | "indent": 0,
498 | "type": "line",
499 | "nodes": [
500 | {
501 | "type": "decoration",
502 | "raw": "[! deco]",
503 | "rawDecos": "!",
504 | "decos": [
505 | "!"
506 | ],
507 | "nodes": [
508 | {
509 | "type": "plain",
510 | "raw": "deco",
511 | "text": "deco"
512 | }
513 | ]
514 | }
515 | ]
516 | },
517 | {
518 | "indent": 0,
519 | "type": "line",
520 | "nodes": [
521 | {
522 | "type": "decoration",
523 | "raw": "[\\" deco]",
524 | "rawDecos": "\\"",
525 | "decos": [
526 | "\\""
527 | ],
528 | "nodes": [
529 | {
530 | "type": "plain",
531 | "raw": "deco",
532 | "text": "deco"
533 | }
534 | ]
535 | }
536 | ]
537 | },
538 | {
539 | "indent": 0,
540 | "type": "line",
541 | "nodes": [
542 | {
543 | "type": "decoration",
544 | "raw": "[# deco]",
545 | "rawDecos": "#",
546 | "decos": [
547 | "#"
548 | ],
549 | "nodes": [
550 | {
551 | "type": "plain",
552 | "raw": "deco",
553 | "text": "deco"
554 | }
555 | ]
556 | }
557 | ]
558 | },
559 | {
560 | "indent": 0,
561 | "type": "line",
562 | "nodes": [
563 | {
564 | "type": "decoration",
565 | "raw": "[% deco]",
566 | "rawDecos": "%",
567 | "decos": [
568 | "%"
569 | ],
570 | "nodes": [
571 | {
572 | "type": "plain",
573 | "raw": "deco",
574 | "text": "deco"
575 | }
576 | ]
577 | }
578 | ]
579 | },
580 | {
581 | "indent": 0,
582 | "type": "line",
583 | "nodes": [
584 | {
585 | "type": "decoration",
586 | "raw": "[& deco]",
587 | "rawDecos": "&",
588 | "decos": [
589 | "&"
590 | ],
591 | "nodes": [
592 | {
593 | "type": "plain",
594 | "raw": "deco",
595 | "text": "deco"
596 | }
597 | ]
598 | }
599 | ]
600 | },
601 | {
602 | "indent": 0,
603 | "type": "line",
604 | "nodes": [
605 | {
606 | "type": "decoration",
607 | "raw": "[' deco]",
608 | "rawDecos": "'",
609 | "decos": [
610 | "'"
611 | ],
612 | "nodes": [
613 | {
614 | "type": "plain",
615 | "raw": "deco",
616 | "text": "deco"
617 | }
618 | ]
619 | }
620 | ]
621 | },
622 | {
623 | "indent": 0,
624 | "type": "line",
625 | "nodes": [
626 | {
627 | "type": "decoration",
628 | "raw": "[( deco]",
629 | "rawDecos": "(",
630 | "decos": [
631 | "("
632 | ],
633 | "nodes": [
634 | {
635 | "type": "plain",
636 | "raw": "deco",
637 | "text": "deco"
638 | }
639 | ]
640 | }
641 | ]
642 | },
643 | {
644 | "indent": 0,
645 | "type": "line",
646 | "nodes": [
647 | {
648 | "type": "decoration",
649 | "raw": "[) deco]",
650 | "rawDecos": ")",
651 | "decos": [
652 | ")"
653 | ],
654 | "nodes": [
655 | {
656 | "type": "plain",
657 | "raw": "deco",
658 | "text": "deco"
659 | }
660 | ]
661 | }
662 | ]
663 | },
664 | {
665 | "indent": 0,
666 | "type": "line",
667 | "nodes": [
668 | {
669 | "type": "decoration",
670 | "raw": "[+ deco]",
671 | "rawDecos": "+",
672 | "decos": [
673 | "+"
674 | ],
675 | "nodes": [
676 | {
677 | "type": "plain",
678 | "raw": "deco",
679 | "text": "deco"
680 | }
681 | ]
682 | }
683 | ]
684 | },
685 | {
686 | "indent": 0,
687 | "type": "line",
688 | "nodes": [
689 | {
690 | "type": "decoration",
691 | "raw": "[, deco]",
692 | "rawDecos": ",",
693 | "decos": [
694 | ","
695 | ],
696 | "nodes": [
697 | {
698 | "type": "plain",
699 | "raw": "deco",
700 | "text": "deco"
701 | }
702 | ]
703 | }
704 | ]
705 | },
706 | {
707 | "indent": 0,
708 | "type": "line",
709 | "nodes": [
710 | {
711 | "type": "decoration",
712 | "raw": "[- deco]",
713 | "rawDecos": "-",
714 | "decos": [
715 | "-"
716 | ],
717 | "nodes": [
718 | {
719 | "type": "plain",
720 | "raw": "deco",
721 | "text": "deco"
722 | }
723 | ]
724 | }
725 | ]
726 | },
727 | {
728 | "indent": 0,
729 | "type": "line",
730 | "nodes": [
731 | {
732 | "type": "decoration",
733 | "raw": "[. deco]",
734 | "rawDecos": ".",
735 | "decos": [
736 | "."
737 | ],
738 | "nodes": [
739 | {
740 | "type": "plain",
741 | "raw": "deco",
742 | "text": "deco"
743 | }
744 | ]
745 | }
746 | ]
747 | },
748 | {
749 | "indent": 0,
750 | "type": "line",
751 | "nodes": [
752 | {
753 | "type": "decoration",
754 | "raw": "[/ deco]",
755 | "rawDecos": "/",
756 | "decos": [
757 | "/"
758 | ],
759 | "nodes": [
760 | {
761 | "type": "plain",
762 | "raw": "deco",
763 | "text": "deco"
764 | }
765 | ]
766 | }
767 | ]
768 | },
769 | {
770 | "indent": 0,
771 | "type": "line",
772 | "nodes": [
773 | {
774 | "type": "decoration",
775 | "raw": "[{ deco]",
776 | "rawDecos": "{",
777 | "decos": [
778 | "{"
779 | ],
780 | "nodes": [
781 | {
782 | "type": "plain",
783 | "raw": "deco",
784 | "text": "deco"
785 | }
786 | ]
787 | }
788 | ]
789 | },
790 | {
791 | "indent": 0,
792 | "type": "line",
793 | "nodes": [
794 | {
795 | "type": "decoration",
796 | "raw": "[| deco]",
797 | "rawDecos": "|",
798 | "decos": [
799 | "|"
800 | ],
801 | "nodes": [
802 | {
803 | "type": "plain",
804 | "raw": "deco",
805 | "text": "deco"
806 | }
807 | ]
808 | }
809 | ]
810 | },
811 | {
812 | "indent": 0,
813 | "type": "line",
814 | "nodes": [
815 | {
816 | "type": "decoration",
817 | "raw": "[} deco]",
818 | "rawDecos": "}",
819 | "decos": [
820 | "}"
821 | ],
822 | "nodes": [
823 | {
824 | "type": "plain",
825 | "raw": "deco",
826 | "text": "deco"
827 | }
828 | ]
829 | }
830 | ]
831 | },
832 | {
833 | "indent": 0,
834 | "type": "line",
835 | "nodes": [
836 | {
837 | "type": "decoration",
838 | "raw": "[< deco]",
839 | "rawDecos": "<",
840 | "decos": [
841 | "<"
842 | ],
843 | "nodes": [
844 | {
845 | "type": "plain",
846 | "raw": "deco",
847 | "text": "deco"
848 | }
849 | ]
850 | }
851 | ]
852 | },
853 | {
854 | "indent": 0,
855 | "type": "line",
856 | "nodes": [
857 | {
858 | "type": "decoration",
859 | "raw": "[> deco]",
860 | "rawDecos": ">",
861 | "decos": [
862 | ">"
863 | ],
864 | "nodes": [
865 | {
866 | "type": "plain",
867 | "raw": "deco",
868 | "text": "deco"
869 | }
870 | ]
871 | }
872 | ]
873 | },
874 | {
875 | "indent": 0,
876 | "type": "line",
877 | "nodes": [
878 | {
879 | "type": "decoration",
880 | "raw": "[_ deco]",
881 | "rawDecos": "_",
882 | "decos": [
883 | "_"
884 | ],
885 | "nodes": [
886 | {
887 | "type": "plain",
888 | "raw": "deco",
889 | "text": "deco"
890 | }
891 | ]
892 | }
893 | ]
894 | },
895 | {
896 | "indent": 0,
897 | "type": "line",
898 | "nodes": [
899 | {
900 | "type": "decoration",
901 | "raw": "[~ deco]",
902 | "rawDecos": "~",
903 | "decos": [
904 | "~"
905 | ],
906 | "nodes": [
907 | {
908 | "type": "plain",
909 | "raw": "deco",
910 | "text": "deco"
911 | }
912 | ]
913 | }
914 | ]
915 | }
916 | ]
917 | `;
918 |
--------------------------------------------------------------------------------
/test/line/formula.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("formula", () => {
5 | it("Simple formula", ({ assert }) => {
6 | assert.snapshot(
7 | parse("[$ \\frac{3}{2}^N]", {
8 | hasTitle: false,
9 | }),
10 | );
11 | });
12 |
13 | it("Formula includes [] with tail half-space", ({ assert }) => {
14 | assert.snapshot(parse("[$ [x] ]", { hasTitle: false }));
15 | });
16 |
17 | it("Formula includes [] without tail half-space", ({ assert }) => {
18 | assert.snapshot(parse("[$ [x]]", { hasTitle: false }));
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/test/line/formula.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`formula > Formula includes [] with tail half-space 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "formula",
9 | "raw": "[$ [x] ]",
10 | "formula": "[x]"
11 | }
12 | ]
13 | }
14 | ]
15 | `;
16 |
17 | exports[`formula > Formula includes [] without tail half-space 1`] = `
18 | [
19 | {
20 | "indent": 0,
21 | "type": "line",
22 | "nodes": [
23 | {
24 | "type": "formula",
25 | "raw": "[$ [x]",
26 | "formula": "[x"
27 | },
28 | {
29 | "type": "plain",
30 | "raw": "]",
31 | "text": "]"
32 | }
33 | ]
34 | }
35 | ]
36 | `;
37 |
38 | exports[`formula > Simple formula 1`] = `
39 | [
40 | {
41 | "indent": 0,
42 | "type": "line",
43 | "nodes": [
44 | {
45 | "type": "formula",
46 | "raw": "[$ \\\\frac{3}{2}^N]",
47 | "formula": "\\\\frac{3}{2}^N"
48 | }
49 | ]
50 | }
51 | ]
52 | `;
53 |
--------------------------------------------------------------------------------
/test/line/googleMap.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("googleMap", () => {
5 | it("Simple google map with NE", ({ assert }) => {
6 | assert.snapshot(
7 | parse("[N35.6812362,E139.7649361]", {
8 | hasTitle: false,
9 | }),
10 | );
11 | });
12 |
13 | it("Simple google map with SW", ({ assert }) => {
14 | assert.snapshot(
15 | parse("[S13.70533,W69.6533372]", {
16 | hasTitle: false,
17 | }),
18 | );
19 | });
20 |
21 | it("Simple google map with zoom", ({ assert }) => {
22 | assert.snapshot(
23 | parse("[N35.6812362,E139.7649361,Z14]", {
24 | hasTitle: false,
25 | }),
26 | );
27 | });
28 |
29 | it("Simple google map with place on left", ({ assert }) => {
30 | assert.snapshot(
31 | parse("[東京駅 N35.6812362,E139.7649361,Z14]", {
32 | hasTitle: false,
33 | }),
34 | );
35 | });
36 |
37 | it("Simple google map with place on right", ({ assert }) => {
38 | assert.snapshot(
39 | parse("[N35.6812362,E139.7649361,Z14 東京駅]", {
40 | hasTitle: false,
41 | }),
42 | );
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/test/line/googleMap.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`googleMap > Simple google map with NE 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "googleMap",
9 | "raw": "[N35.6812362,E139.7649361]",
10 | "latitude": 35.6812362,
11 | "longitude": 139.7649361,
12 | "zoom": 14,
13 | "place": "",
14 | "url": "https://www.google.com/maps/@35.6812362,139.7649361,14z"
15 | }
16 | ]
17 | }
18 | ]
19 | `;
20 |
21 | exports[`googleMap > Simple google map with SW 1`] = `
22 | [
23 | {
24 | "indent": 0,
25 | "type": "line",
26 | "nodes": [
27 | {
28 | "type": "googleMap",
29 | "raw": "[S13.70533,W69.6533372]",
30 | "latitude": -13.70533,
31 | "longitude": -69.6533372,
32 | "zoom": 14,
33 | "place": "",
34 | "url": "https://www.google.com/maps/@-13.70533,-69.6533372,14z"
35 | }
36 | ]
37 | }
38 | ]
39 | `;
40 |
41 | exports[`googleMap > Simple google map with place on left 1`] = `
42 | [
43 | {
44 | "indent": 0,
45 | "type": "line",
46 | "nodes": [
47 | {
48 | "type": "googleMap",
49 | "raw": "[東京駅 N35.6812362,E139.7649361,Z14]",
50 | "latitude": 35.6812362,
51 | "longitude": 139.7649361,
52 | "zoom": 14,
53 | "place": "東京駅",
54 | "url": "https://www.google.com/maps/place/%E6%9D%B1%E4%BA%AC%E9%A7%85/@35.6812362,139.7649361,14z"
55 | }
56 | ]
57 | }
58 | ]
59 | `;
60 |
61 | exports[`googleMap > Simple google map with place on right 1`] = `
62 | [
63 | {
64 | "indent": 0,
65 | "type": "line",
66 | "nodes": [
67 | {
68 | "type": "googleMap",
69 | "raw": "[N35.6812362,E139.7649361,Z14 東京駅]",
70 | "latitude": 35.6812362,
71 | "longitude": 139.7649361,
72 | "zoom": 14,
73 | "place": "東京駅",
74 | "url": "https://www.google.com/maps/place/%E6%9D%B1%E4%BA%AC%E9%A7%85/@35.6812362,139.7649361,14z"
75 | }
76 | ]
77 | }
78 | ]
79 | `;
80 |
81 | exports[`googleMap > Simple google map with zoom 1`] = `
82 | [
83 | {
84 | "indent": 0,
85 | "type": "line",
86 | "nodes": [
87 | {
88 | "type": "googleMap",
89 | "raw": "[N35.6812362,E139.7649361,Z14]",
90 | "latitude": 35.6812362,
91 | "longitude": 139.7649361,
92 | "zoom": 14,
93 | "place": "",
94 | "url": "https://www.google.com/maps/@35.6812362,139.7649361,14z"
95 | }
96 | ]
97 | }
98 | ]
99 | `;
100 |
--------------------------------------------------------------------------------
/test/line/hashTag.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("hashTag", () => {
5 | it("Simple hashTag", ({ assert }) => {
6 | assert.snapshot(parse("#tag", { hasTitle: false }));
7 | });
8 |
9 | it("Only `#` is not hashTag", ({ assert }) => {
10 | assert.snapshot(parse("#", { hasTitle: false }));
11 | });
12 |
13 | it("HashTag includes `#`", ({ assert }) => {
14 | assert.snapshot(parse("#hash#Tag", { hasTitle: false }));
15 | });
16 |
17 | it("HashTag in sentence with spaces", ({ assert }) => {
18 | assert.snapshot(parse("This is a #tag .", { hasTitle: false }));
19 | });
20 |
21 | it("HashTag in sentence without spaces is not hashTag", ({ assert }) => {
22 | assert.snapshot(parse("→#notTag←", { hasTitle: false }));
23 | });
24 |
25 | it("Multiple hashTag", ({ assert }) => {
26 | assert.snapshot(parse("#hoge #fuga #piyo", { hasTitle: false }));
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/test/line/hashTag.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`hashTag > HashTag in sentence with spaces 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "plain",
9 | "raw": "This is a",
10 | "text": "This is a"
11 | },
12 | {
13 | "type": "plain",
14 | "raw": " ",
15 | "text": " "
16 | },
17 | {
18 | "type": "hashTag",
19 | "raw": "#tag",
20 | "href": "tag"
21 | },
22 | {
23 | "type": "plain",
24 | "raw": " .",
25 | "text": " ."
26 | }
27 | ]
28 | }
29 | ]
30 | `;
31 |
32 | exports[`hashTag > HashTag in sentence without spaces is not hashTag 1`] = `
33 | [
34 | {
35 | "indent": 0,
36 | "type": "line",
37 | "nodes": [
38 | {
39 | "type": "plain",
40 | "raw": "→#notTag←",
41 | "text": "→#notTag←"
42 | }
43 | ]
44 | }
45 | ]
46 | `;
47 |
48 | exports[`hashTag > HashTag includes \`#\` 1`] = `
49 | [
50 | {
51 | "indent": 0,
52 | "type": "line",
53 | "nodes": [
54 | {
55 | "type": "hashTag",
56 | "raw": "#hash#Tag",
57 | "href": "hash#Tag"
58 | }
59 | ]
60 | }
61 | ]
62 | `;
63 |
64 | exports[`hashTag > Multiple hashTag 1`] = `
65 | [
66 | {
67 | "indent": 0,
68 | "type": "line",
69 | "nodes": [
70 | {
71 | "type": "hashTag",
72 | "raw": "#hoge",
73 | "href": "hoge"
74 | },
75 | {
76 | "type": "plain",
77 | "raw": " ",
78 | "text": " "
79 | },
80 | {
81 | "type": "hashTag",
82 | "raw": "#fuga",
83 | "href": "fuga"
84 | },
85 | {
86 | "type": "plain",
87 | "raw": " ",
88 | "text": " "
89 | },
90 | {
91 | "type": "hashTag",
92 | "raw": "#piyo",
93 | "href": "piyo"
94 | }
95 | ]
96 | }
97 | ]
98 | `;
99 |
100 | exports[`hashTag > Only \`#\` is not hashTag 1`] = `
101 | [
102 | {
103 | "indent": 0,
104 | "type": "line",
105 | "nodes": [
106 | {
107 | "type": "plain",
108 | "raw": "#",
109 | "text": "#"
110 | }
111 | ]
112 | }
113 | ]
114 | `;
115 |
116 | exports[`hashTag > Simple hashTag 1`] = `
117 | [
118 | {
119 | "indent": 0,
120 | "type": "line",
121 | "nodes": [
122 | {
123 | "type": "hashTag",
124 | "raw": "#tag",
125 | "href": "tag"
126 | }
127 | ]
128 | }
129 | ]
130 | `;
131 |
--------------------------------------------------------------------------------
/test/line/helpfeel.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("helpfeel", () => {
5 | it("Simple helpfeel", ({ assert }) => {
6 | assert.snapshot(parse("? Simple helpfeel", { hasTitle: false }));
7 | });
8 |
9 | it("No head `?` is not helpfeel", ({ assert }) => {
10 | assert.snapshot(parse("a ? not helpfeel", { hasTitle: false }));
11 | });
12 |
13 | it("Quoted ? is not helpfeel", ({ assert }) => {
14 | assert.snapshot(parse("> ? Quoted", { hasTitle: false }));
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/test/line/helpfeel.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`helpfeel > No head \`?\` is not helpfeel 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "plain",
9 | "raw": "a ? not helpfeel",
10 | "text": "a ? not helpfeel"
11 | }
12 | ]
13 | }
14 | ]
15 | `;
16 |
17 | exports[`helpfeel > Quoted ? is not helpfeel 1`] = `
18 | [
19 | {
20 | "indent": 0,
21 | "type": "line",
22 | "nodes": [
23 | {
24 | "type": "quote",
25 | "raw": "> ? Quoted",
26 | "nodes": [
27 | {
28 | "type": "plain",
29 | "raw": " ? Quoted",
30 | "text": " ? Quoted"
31 | }
32 | ]
33 | }
34 | ]
35 | }
36 | ]
37 | `;
38 |
39 | exports[`helpfeel > Simple helpfeel 1`] = `
40 | [
41 | {
42 | "indent": 0,
43 | "type": "line",
44 | "nodes": [
45 | {
46 | "type": "helpfeel",
47 | "raw": "? Simple helpfeel",
48 | "text": "Simple helpfeel"
49 | }
50 | ]
51 | }
52 | ]
53 | `;
54 |
--------------------------------------------------------------------------------
/test/line/icon.test.ts:
--------------------------------------------------------------------------------
1 | import { deepStrictEqual, strictEqual } from "node:assert/strict";
2 | import { describe, it } from "node:test";
3 | import { parse } from "../../src/index.ts";
4 |
5 | describe("icon", () => {
6 | it("Simple root icon", ({ assert }) => {
7 | assert.snapshot(parse("[/icons/+1.icon]", { hasTitle: false }));
8 | });
9 |
10 | it("Simple relative icon", ({ assert }) => {
11 | assert.snapshot(parse("[me.icon]", { hasTitle: false }));
12 | });
13 |
14 | it("Multiple icons", ({ assert }) => {
15 | assert.snapshot(parse("[me.icon*3]", { hasTitle: false }));
16 | });
17 |
18 | it("Icon and internal link on same line", ({ assert }) => {
19 | assert.snapshot(
20 | parse("[Internal link][me.icon]", {
21 | hasTitle: false,
22 | }),
23 | );
24 | });
25 |
26 | it("Each multiple icon must be different Object", () => {
27 | const [block] = parse("[me.icon*2]", { hasTitle: false });
28 |
29 | if (block === undefined || block.type !== "line") {
30 | throw new Error("fail");
31 | }
32 |
33 | strictEqual(block.nodes.length, 2);
34 | deepStrictEqual(block.nodes[0], block.nodes[1]);
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/test/line/icon.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`icon > Icon and internal link on same line 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "link",
9 | "raw": "[Internal link]",
10 | "pathType": "relative",
11 | "href": "Internal link",
12 | "content": ""
13 | },
14 | {
15 | "path": "me",
16 | "pathType": "relative",
17 | "type": "icon",
18 | "raw": "[me.icon]"
19 | }
20 | ]
21 | }
22 | ]
23 | `;
24 |
25 | exports[`icon > Multiple icons 1`] = `
26 | [
27 | {
28 | "indent": 0,
29 | "type": "line",
30 | "nodes": [
31 | {
32 | "path": "me",
33 | "pathType": "relative",
34 | "type": "icon",
35 | "raw": "[me.icon*3]"
36 | },
37 | {
38 | "path": "me",
39 | "pathType": "relative",
40 | "type": "icon",
41 | "raw": "[me.icon*3]"
42 | },
43 | {
44 | "path": "me",
45 | "pathType": "relative",
46 | "type": "icon",
47 | "raw": "[me.icon*3]"
48 | }
49 | ]
50 | }
51 | ]
52 | `;
53 |
54 | exports[`icon > Simple relative icon 1`] = `
55 | [
56 | {
57 | "indent": 0,
58 | "type": "line",
59 | "nodes": [
60 | {
61 | "path": "me",
62 | "pathType": "relative",
63 | "type": "icon",
64 | "raw": "[me.icon]"
65 | }
66 | ]
67 | }
68 | ]
69 | `;
70 |
71 | exports[`icon > Simple root icon 1`] = `
72 | [
73 | {
74 | "indent": 0,
75 | "type": "line",
76 | "nodes": [
77 | {
78 | "path": "/icons/+1",
79 | "pathType": "root",
80 | "type": "icon",
81 | "raw": "[/icons/+1.icon]"
82 | }
83 | ]
84 | }
85 | ]
86 | `;
87 |
--------------------------------------------------------------------------------
/test/line/image.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("image", () => {
5 | it("Simple image", ({ assert }) => {
6 | assert.snapshot(
7 | parse(
8 | `
9 | [http://example.com/image.png]
10 | [https://example.com/image.JPG]
11 | `.trim(),
12 | {
13 | hasTitle: false,
14 | },
15 | ),
16 | );
17 | });
18 |
19 | it("HTTP jpeg image with special and japanese chars", ({ assert }) => {
20 | assert.snapshot(
21 | parse("[http://example.com/~!@#$%^&*()_+`-={}\\'\"?,.<>|/画像.jpeg]", {
22 | hasTitle: false,
23 | }),
24 | );
25 | });
26 |
27 | it("HTTPS svg, GIF and WebP image with link", ({ assert }) => {
28 | assert.snapshot(
29 | parse(
30 | `
31 | [https://example.com/image.svg https://example.com/]
32 | [https://example.com/ https://example.com/image.GIF]
33 | [https://example.com/image.webp https://example.com]
34 | `.trim(),
35 | {
36 | hasTitle: false,
37 | },
38 | ),
39 | );
40 | });
41 |
42 | it("Image with double image link", ({ assert }) => {
43 | assert.snapshot(
44 | parse(
45 | "[https://example.com/forward.png https://example.com/backward.png]",
46 | { hasTitle: false },
47 | ),
48 | );
49 | });
50 |
51 | it("Gyazo image", ({ assert }) => {
52 | assert.snapshot(
53 | parse(
54 | `
55 | [https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815]
56 | [https://i.gyazo.com/0f82099330f378fe4917a1b4a5fe8815]
57 | [https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815/raw]
58 | `.trim(),
59 | { hasTitle: false },
60 | ),
61 | );
62 | });
63 |
64 | it("Gyazo image with link", ({ assert }) => {
65 | assert.snapshot(
66 | parse(
67 | `
68 | [https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815 https://example.com]
69 | [https://example.com https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815]
70 | [https://gyazo.com/7057219f5b20ca8afd122945b72453d3 https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815]
71 | `.trim(),
72 | { hasTitle: false },
73 | ),
74 | );
75 | });
76 |
77 | it("Image with GET parameters", ({ assert }) => {
78 | assert.snapshot(
79 | parse("[http://example.com/image.png?key1=value1&key2=value2]", {
80 | hasTitle: false,
81 | }),
82 | );
83 | });
84 |
85 | it("Direct Gyazo image", ({ assert }) => {
86 | assert.snapshot(
87 | parse("[https://i.gyazo.com/0f82099330f378fe4917a1b4a5fe8815.png]", {
88 | hasTitle: false,
89 | }),
90 | );
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/test/line/image.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`image > Direct Gyazo image 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "image",
9 | "raw": "[https://i.gyazo.com/0f82099330f378fe4917a1b4a5fe8815.png]",
10 | "src": "https://i.gyazo.com/0f82099330f378fe4917a1b4a5fe8815.png",
11 | "link": ""
12 | }
13 | ]
14 | }
15 | ]
16 | `;
17 |
18 | exports[`image > Gyazo image 1`] = `
19 | [
20 | {
21 | "indent": 0,
22 | "type": "line",
23 | "nodes": [
24 | {
25 | "type": "image",
26 | "raw": "[https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815]",
27 | "src": "https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815/thumb/1000",
28 | "link": ""
29 | }
30 | ]
31 | },
32 | {
33 | "indent": 0,
34 | "type": "line",
35 | "nodes": [
36 | {
37 | "type": "image",
38 | "raw": "[https://i.gyazo.com/0f82099330f378fe4917a1b4a5fe8815]",
39 | "src": "https://i.gyazo.com/0f82099330f378fe4917a1b4a5fe8815/thumb/1000",
40 | "link": ""
41 | }
42 | ]
43 | },
44 | {
45 | "indent": 0,
46 | "type": "line",
47 | "nodes": [
48 | {
49 | "type": "image",
50 | "raw": "[https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815/raw]",
51 | "src": "https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815/raw",
52 | "link": ""
53 | }
54 | ]
55 | }
56 | ]
57 | `;
58 |
59 | exports[`image > Gyazo image with link 1`] = `
60 | [
61 | {
62 | "indent": 0,
63 | "type": "line",
64 | "nodes": [
65 | {
66 | "type": "image",
67 | "raw": "[https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815 https://example.com]",
68 | "src": "https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815/thumb/1000",
69 | "link": "https://example.com"
70 | }
71 | ]
72 | },
73 | {
74 | "indent": 0,
75 | "type": "line",
76 | "nodes": [
77 | {
78 | "type": "image",
79 | "raw": "[https://example.com https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815]",
80 | "src": "https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815/thumb/1000",
81 | "link": "https://example.com"
82 | }
83 | ]
84 | },
85 | {
86 | "indent": 0,
87 | "type": "line",
88 | "nodes": [
89 | {
90 | "type": "image",
91 | "raw": "[https://gyazo.com/7057219f5b20ca8afd122945b72453d3 https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815]",
92 | "src": "https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815/thumb/1000",
93 | "link": "https://gyazo.com/7057219f5b20ca8afd122945b72453d3"
94 | }
95 | ]
96 | }
97 | ]
98 | `;
99 |
100 | exports[`image > HTTP jpeg image with special and japanese chars 1`] = `
101 | [
102 | {
103 | "indent": 0,
104 | "type": "line",
105 | "nodes": [
106 | {
107 | "type": "image",
108 | "raw": "[http://example.com/~!@#$%^&*()_+\`-={}\\\\'\\"?,.<>|/画像.jpeg]",
109 | "src": "http://example.com/~!@#$%^&*()_+\`-={}\\\\'\\"?,.<>|/画像.jpeg",
110 | "link": ""
111 | }
112 | ]
113 | }
114 | ]
115 | `;
116 |
117 | exports[`image > HTTPS svg, GIF and WebP image with link 1`] = `
118 | [
119 | {
120 | "indent": 0,
121 | "type": "line",
122 | "nodes": [
123 | {
124 | "type": "image",
125 | "raw": "[https://example.com/image.svg https://example.com/]",
126 | "src": "https://example.com/image.svg",
127 | "link": "https://example.com/"
128 | }
129 | ]
130 | },
131 | {
132 | "indent": 0,
133 | "type": "line",
134 | "nodes": [
135 | {
136 | "type": "image",
137 | "raw": "[https://example.com/ https://example.com/image.GIF]",
138 | "src": "https://example.com/image.GIF",
139 | "link": "https://example.com/"
140 | }
141 | ]
142 | },
143 | {
144 | "indent": 0,
145 | "type": "line",
146 | "nodes": [
147 | {
148 | "type": "image",
149 | "raw": "[https://example.com/image.webp https://example.com]",
150 | "src": "https://example.com/image.webp",
151 | "link": "https://example.com"
152 | }
153 | ]
154 | }
155 | ]
156 | `;
157 |
158 | exports[`image > Image with GET parameters 1`] = `
159 | [
160 | {
161 | "indent": 0,
162 | "type": "line",
163 | "nodes": [
164 | {
165 | "type": "image",
166 | "raw": "[http://example.com/image.png?key1=value1&key2=value2]",
167 | "src": "http://example.com/image.png?key1=value1&key2=value2",
168 | "link": ""
169 | }
170 | ]
171 | }
172 | ]
173 | `;
174 |
175 | exports[`image > Image with double image link 1`] = `
176 | [
177 | {
178 | "indent": 0,
179 | "type": "line",
180 | "nodes": [
181 | {
182 | "type": "image",
183 | "raw": "[https://example.com/forward.png https://example.com/backward.png]",
184 | "src": "https://example.com/backward.png",
185 | "link": "https://example.com/forward.png"
186 | }
187 | ]
188 | }
189 | ]
190 | `;
191 |
192 | exports[`image > Simple image 1`] = `
193 | [
194 | {
195 | "indent": 0,
196 | "type": "line",
197 | "nodes": [
198 | {
199 | "type": "image",
200 | "raw": "[http://example.com/image.png]",
201 | "src": "http://example.com/image.png",
202 | "link": ""
203 | }
204 | ]
205 | },
206 | {
207 | "indent": 0,
208 | "type": "line",
209 | "nodes": [
210 | {
211 | "type": "image",
212 | "raw": "[https://example.com/image.JPG]",
213 | "src": "https://example.com/image.JPG",
214 | "link": ""
215 | }
216 | ]
217 | }
218 | ]
219 | `;
220 |
--------------------------------------------------------------------------------
/test/line/index.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("line", () => {
5 | it("Line that have multi node", ({ assert }) => {
6 | assert.snapshot(parse("[Link][Link]", { hasTitle: false }));
7 | });
8 |
9 | it("Decoration line includes internal link", ({ assert }) => {
10 | assert.snapshot(parse("[* [Link]]", { hasTitle: false }));
11 | });
12 |
13 | it("Decoration line includes external link", ({ assert }) => {
14 | assert.snapshot(
15 | parse("[* [https://example.com example]]", {
16 | hasTitle: false,
17 | }),
18 | );
19 | });
20 |
21 | it("Multi `]`", ({ assert }) => {
22 | assert.snapshot(
23 | parse("[* [Link]`code`[Link]]", {
24 | hasTitle: false,
25 | }),
26 | );
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/test/line/index.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`line > Decoration line includes external link 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "decoration",
9 | "raw": "[* [https://example.com example]]",
10 | "rawDecos": "*",
11 | "decos": [
12 | "*-1"
13 | ],
14 | "nodes": [
15 | {
16 | "type": "link",
17 | "raw": "[https://example.com example]",
18 | "pathType": "absolute",
19 | "href": "https://example.com",
20 | "content": "example"
21 | }
22 | ]
23 | }
24 | ]
25 | }
26 | ]
27 | `;
28 |
29 | exports[`line > Decoration line includes internal link 1`] = `
30 | [
31 | {
32 | "indent": 0,
33 | "type": "line",
34 | "nodes": [
35 | {
36 | "type": "decoration",
37 | "raw": "[* [Link]]",
38 | "rawDecos": "*",
39 | "decos": [
40 | "*-1"
41 | ],
42 | "nodes": [
43 | {
44 | "type": "link",
45 | "raw": "[Link]",
46 | "pathType": "relative",
47 | "href": "Link",
48 | "content": ""
49 | }
50 | ]
51 | }
52 | ]
53 | }
54 | ]
55 | `;
56 |
57 | exports[`line > Line that have multi node 1`] = `
58 | [
59 | {
60 | "indent": 0,
61 | "type": "line",
62 | "nodes": [
63 | {
64 | "type": "link",
65 | "raw": "[Link]",
66 | "pathType": "relative",
67 | "href": "Link",
68 | "content": ""
69 | },
70 | {
71 | "type": "link",
72 | "raw": "[Link]",
73 | "pathType": "relative",
74 | "href": "Link",
75 | "content": ""
76 | }
77 | ]
78 | }
79 | ]
80 | `;
81 |
82 | exports[`line > Multi \`]\` 1`] = `
83 | [
84 | {
85 | "indent": 0,
86 | "type": "line",
87 | "nodes": [
88 | {
89 | "type": "decoration",
90 | "raw": "[* [Link]",
91 | "rawDecos": "*",
92 | "decos": [
93 | "*-1"
94 | ],
95 | "nodes": [
96 | {
97 | "type": "plain",
98 | "raw": "[Link",
99 | "text": "[Link"
100 | }
101 | ]
102 | },
103 | {
104 | "type": "code",
105 | "raw": "\`code\`",
106 | "text": "code"
107 | },
108 | {
109 | "type": "link",
110 | "raw": "[Link]",
111 | "pathType": "relative",
112 | "href": "Link",
113 | "content": ""
114 | },
115 | {
116 | "type": "plain",
117 | "raw": "]",
118 | "text": "]"
119 | }
120 | ]
121 | }
122 | ]
123 | `;
124 |
--------------------------------------------------------------------------------
/test/line/link.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("link", () => {
5 | it("Simple absolute link", ({ assert }) => {
6 | assert.snapshot(
7 | parse("https://example.com/", {
8 | hasTitle: false,
9 | }),
10 | );
11 | });
12 |
13 | it("Simple absolute link with ahead non-space character", ({ assert }) => {
14 | assert.snapshot(
15 | parse("ahttps://example.com/", {
16 | hasTitle: false,
17 | }),
18 | );
19 | });
20 |
21 | it("Simple absolute link with bracket", ({ assert }) => {
22 | assert.snapshot(
23 | parse("[https://example.com/]", {
24 | hasTitle: false,
25 | }),
26 | );
27 | });
28 |
29 | it("Simple root link", ({ assert }) => {
30 | assert.snapshot(parse("[/project/page]", { hasTitle: false }));
31 | });
32 |
33 | it("Simple relative link", ({ assert }) => {
34 | assert.snapshot(parse("[page]", { hasTitle: false }));
35 | });
36 |
37 | it("Link with content", ({ assert }) => {
38 | assert.snapshot(
39 | parse(
40 | `
41 | [https://example.com/ Example]
42 | [Example https://example.com/]
43 | [https://left.com/ center https://right.com/]
44 | `.trim(),
45 | {
46 | hasTitle: false,
47 | },
48 | ),
49 | );
50 | });
51 |
52 | it("Root and relative link path can include space", ({ assert }) => {
53 | assert.snapshot(
54 | parse(
55 | `
56 | [page name]
57 | [/project/page name]
58 | `.trim(),
59 | { hasTitle: false },
60 | ),
61 | );
62 | });
63 |
64 | it("Link with link", ({ assert }) => {
65 | assert.snapshot(
66 | parse("[https://example.com https://example.com]", { hasTitle: false }),
67 | );
68 | });
69 |
70 | it("Link with GET parameters", ({ assert }) => {
71 | assert.snapshot(
72 | parse("[http://example.com?key1=value1&key2=value2]", {
73 | hasTitle: false,
74 | }),
75 | );
76 | });
77 | });
78 |
--------------------------------------------------------------------------------
/test/line/link.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`link > Link with GET parameters 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "link",
9 | "raw": "[http://example.com?key1=value1&key2=value2]",
10 | "pathType": "absolute",
11 | "href": "http://example.com?key1=value1&key2=value2",
12 | "content": ""
13 | }
14 | ]
15 | }
16 | ]
17 | `;
18 |
19 | exports[`link > Link with content 1`] = `
20 | [
21 | {
22 | "indent": 0,
23 | "type": "line",
24 | "nodes": [
25 | {
26 | "type": "link",
27 | "raw": "[https://example.com/ Example]",
28 | "pathType": "absolute",
29 | "href": "https://example.com/",
30 | "content": "Example"
31 | }
32 | ]
33 | },
34 | {
35 | "indent": 0,
36 | "type": "line",
37 | "nodes": [
38 | {
39 | "type": "link",
40 | "raw": "[Example https://example.com/]",
41 | "pathType": "absolute",
42 | "href": "https://example.com/",
43 | "content": "Example"
44 | }
45 | ]
46 | },
47 | {
48 | "indent": 0,
49 | "type": "line",
50 | "nodes": [
51 | {
52 | "type": "link",
53 | "raw": "[https://left.com/ center https://right.com/]",
54 | "pathType": "absolute",
55 | "href": "https://left.com/",
56 | "content": "center https://right.com/"
57 | }
58 | ]
59 | }
60 | ]
61 | `;
62 |
63 | exports[`link > Link with link 1`] = `
64 | [
65 | {
66 | "indent": 0,
67 | "type": "line",
68 | "nodes": [
69 | {
70 | "type": "link",
71 | "raw": "[https://example.com https://example.com]",
72 | "pathType": "absolute",
73 | "href": "https://example.com",
74 | "content": "https://example.com"
75 | }
76 | ]
77 | }
78 | ]
79 | `;
80 |
81 | exports[`link > Root and relative link path can include space 1`] = `
82 | [
83 | {
84 | "indent": 0,
85 | "type": "line",
86 | "nodes": [
87 | {
88 | "type": "link",
89 | "raw": "[page name]",
90 | "pathType": "relative",
91 | "href": "page name",
92 | "content": ""
93 | }
94 | ]
95 | },
96 | {
97 | "indent": 0,
98 | "type": "line",
99 | "nodes": [
100 | {
101 | "type": "link",
102 | "raw": "[/project/page name]",
103 | "pathType": "root",
104 | "href": "/project/page name",
105 | "content": ""
106 | }
107 | ]
108 | }
109 | ]
110 | `;
111 |
112 | exports[`link > Simple absolute link 1`] = `
113 | [
114 | {
115 | "indent": 0,
116 | "type": "line",
117 | "nodes": [
118 | {
119 | "type": "link",
120 | "raw": "https://example.com/",
121 | "pathType": "absolute",
122 | "href": "https://example.com/",
123 | "content": ""
124 | }
125 | ]
126 | }
127 | ]
128 | `;
129 |
130 | exports[`link > Simple absolute link with ahead non-space character 1`] = `
131 | [
132 | {
133 | "indent": 0,
134 | "type": "line",
135 | "nodes": [
136 | {
137 | "type": "plain",
138 | "raw": "a",
139 | "text": "a"
140 | },
141 | {
142 | "type": "link",
143 | "raw": "https://example.com/",
144 | "pathType": "absolute",
145 | "href": "https://example.com/",
146 | "content": ""
147 | }
148 | ]
149 | }
150 | ]
151 | `;
152 |
153 | exports[`link > Simple absolute link with bracket 1`] = `
154 | [
155 | {
156 | "indent": 0,
157 | "type": "line",
158 | "nodes": [
159 | {
160 | "type": "link",
161 | "raw": "[https://example.com/]",
162 | "pathType": "absolute",
163 | "href": "https://example.com/",
164 | "content": ""
165 | }
166 | ]
167 | }
168 | ]
169 | `;
170 |
171 | exports[`link > Simple relative link 1`] = `
172 | [
173 | {
174 | "indent": 0,
175 | "type": "line",
176 | "nodes": [
177 | {
178 | "type": "link",
179 | "raw": "[page]",
180 | "pathType": "relative",
181 | "href": "page",
182 | "content": ""
183 | }
184 | ]
185 | }
186 | ]
187 | `;
188 |
189 | exports[`link > Simple root link 1`] = `
190 | [
191 | {
192 | "indent": 0,
193 | "type": "line",
194 | "nodes": [
195 | {
196 | "type": "link",
197 | "raw": "[/project/page]",
198 | "pathType": "root",
199 | "href": "/project/page",
200 | "content": ""
201 | }
202 | ]
203 | }
204 | ]
205 | `;
206 |
--------------------------------------------------------------------------------
/test/line/numberList.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("numberList", () => {
5 | it("Minimum numberList", ({ assert }) => {
6 | assert.snapshot(
7 | parse("1. ", {
8 | hasTitle: false,
9 | }),
10 | );
11 | });
12 |
13 | it("Simple numberList", ({ assert }) => {
14 | assert.snapshot(
15 | parse("1. Simple numberList", {
16 | hasTitle: false,
17 | }),
18 | );
19 | });
20 |
21 | it("1. with decoration", ({ assert }) => {
22 | assert.snapshot(
23 | parse("1. [* deco]", {
24 | hasTitle: false,
25 | }),
26 | );
27 | });
28 |
29 | it("1. with code", ({ assert }) => {
30 | assert.snapshot(
31 | parse("1. `code`", {
32 | hasTitle: false,
33 | }),
34 | );
35 | });
36 |
37 | it("1. with no space is not numberList", ({ assert }) => {
38 | assert.snapshot(
39 | parse("1.not numberList", {
40 | hasTitle: false,
41 | }),
42 | );
43 | });
44 |
45 | it("No head 1. is not numberList", ({ assert }) => {
46 | assert.snapshot(
47 | parse("a 1. not numberList", {
48 | hasTitle: false,
49 | }),
50 | );
51 | });
52 |
53 | it("Quoted 1. is not numberList", ({ assert }) => {
54 | assert.snapshot(parse("> 1. Quoted", { hasTitle: false }));
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/test/line/numberList.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`numberList > 1. with code 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "numberList",
9 | "raw": "1. \`code\`",
10 | "rawNumber": "1",
11 | "number": 1,
12 | "nodes": [
13 | {
14 | "type": "code",
15 | "raw": "\`code\`",
16 | "text": "code"
17 | }
18 | ]
19 | }
20 | ]
21 | }
22 | ]
23 | `;
24 |
25 | exports[`numberList > 1. with decoration 1`] = `
26 | [
27 | {
28 | "indent": 0,
29 | "type": "line",
30 | "nodes": [
31 | {
32 | "type": "numberList",
33 | "raw": "1. [* deco]",
34 | "rawNumber": "1",
35 | "number": 1,
36 | "nodes": [
37 | {
38 | "type": "decoration",
39 | "raw": "[* deco]",
40 | "rawDecos": "*",
41 | "decos": [
42 | "*-1"
43 | ],
44 | "nodes": [
45 | {
46 | "type": "plain",
47 | "raw": "deco",
48 | "text": "deco"
49 | }
50 | ]
51 | }
52 | ]
53 | }
54 | ]
55 | }
56 | ]
57 | `;
58 |
59 | exports[`numberList > 1. with no space is not numberList 1`] = `
60 | [
61 | {
62 | "indent": 0,
63 | "type": "line",
64 | "nodes": [
65 | {
66 | "type": "plain",
67 | "raw": "1.not numberList",
68 | "text": "1.not numberList"
69 | }
70 | ]
71 | }
72 | ]
73 | `;
74 |
75 | exports[`numberList > Minimum numberList 1`] = `
76 | [
77 | {
78 | "indent": 0,
79 | "type": "line",
80 | "nodes": [
81 | {
82 | "type": "numberList",
83 | "raw": "1. ",
84 | "rawNumber": "1",
85 | "number": 1,
86 | "nodes": []
87 | }
88 | ]
89 | }
90 | ]
91 | `;
92 |
93 | exports[`numberList > No head 1. is not numberList 1`] = `
94 | [
95 | {
96 | "indent": 0,
97 | "type": "line",
98 | "nodes": [
99 | {
100 | "type": "plain",
101 | "raw": "a 1. not numberList",
102 | "text": "a 1. not numberList"
103 | }
104 | ]
105 | }
106 | ]
107 | `;
108 |
109 | exports[`numberList > Quoted 1. is not numberList 1`] = `
110 | [
111 | {
112 | "indent": 0,
113 | "type": "line",
114 | "nodes": [
115 | {
116 | "type": "quote",
117 | "raw": "> 1. Quoted",
118 | "nodes": [
119 | {
120 | "type": "plain",
121 | "raw": " 1. Quoted",
122 | "text": " 1. Quoted"
123 | }
124 | ]
125 | }
126 | ]
127 | }
128 | ]
129 | `;
130 |
131 | exports[`numberList > Simple numberList 1`] = `
132 | [
133 | {
134 | "indent": 0,
135 | "type": "line",
136 | "nodes": [
137 | {
138 | "type": "numberList",
139 | "raw": "1. Simple numberList",
140 | "rawNumber": "1",
141 | "number": 1,
142 | "nodes": [
143 | {
144 | "type": "plain",
145 | "raw": "Simple numberList",
146 | "text": "Simple numberList"
147 | }
148 | ]
149 | }
150 | ]
151 | }
152 | ]
153 | `;
154 |
--------------------------------------------------------------------------------
/test/line/plain.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("plain", () => {
5 | it("Simple plain text", ({ assert }) => {
6 | assert.snapshot(parse("Plain text", { hasTitle: false }));
7 | });
8 |
9 | it("Blank line", ({ assert }) => {
10 | assert.snapshot(parse("", { hasTitle: false }));
11 | });
12 |
13 | it("Keep tail space", ({ assert }) => {
14 | assert.snapshot(parse("Tail space -> ", { hasTitle: false }));
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/test/line/plain.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`plain > Blank line 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": []
7 | }
8 | ]
9 | `;
10 |
11 | exports[`plain > Keep tail space 1`] = `
12 | [
13 | {
14 | "indent": 0,
15 | "type": "line",
16 | "nodes": [
17 | {
18 | "type": "plain",
19 | "raw": "Tail space -> ",
20 | "text": "Tail space -> "
21 | }
22 | ]
23 | }
24 | ]
25 | `;
26 |
27 | exports[`plain > Simple plain text 1`] = `
28 | [
29 | {
30 | "indent": 0,
31 | "type": "line",
32 | "nodes": [
33 | {
34 | "type": "plain",
35 | "raw": "Plain text",
36 | "text": "Plain text"
37 | }
38 | ]
39 | }
40 | ]
41 | `;
42 |
--------------------------------------------------------------------------------
/test/line/quote.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("quote", () => {
5 | it("Simple quote", ({ assert }) => {
6 | assert.snapshot(parse("> Simple quote", { hasTitle: false }));
7 | });
8 |
9 | it("Empty quote", ({ assert }) => {
10 | assert.snapshot(parse(">", { hasTitle: false }));
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/test/line/quote.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`quote > Empty quote 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "quote",
9 | "raw": ">",
10 | "nodes": []
11 | }
12 | ]
13 | }
14 | ]
15 | `;
16 |
17 | exports[`quote > Simple quote 1`] = `
18 | [
19 | {
20 | "indent": 0,
21 | "type": "line",
22 | "nodes": [
23 | {
24 | "type": "quote",
25 | "raw": "> Simple quote",
26 | "nodes": [
27 | {
28 | "type": "plain",
29 | "raw": " Simple quote",
30 | "text": " Simple quote"
31 | }
32 | ]
33 | }
34 | ]
35 | }
36 | ]
37 | `;
38 |
--------------------------------------------------------------------------------
/test/line/strong.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("strong", () => {
5 | it("Simple strong", ({ assert }) => {
6 | assert.snapshot(parse("[[Simple strong]]", { hasTitle: false }));
7 | });
8 |
9 | it("[[]] is not strong", ({ assert }) => {
10 | assert.snapshot(parse("[[]]", { hasTitle: false }));
11 | });
12 |
13 | it("Decoration in Strong notation", ({ assert }) => {
14 | assert.snapshot(parse("[[[! deco]]]", { hasTitle: false }));
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/test/line/strong.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`strong > Decoration in Strong notation 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "plain",
9 | "raw": "[[",
10 | "text": "[["
11 | },
12 | {
13 | "type": "decoration",
14 | "raw": "[! deco]",
15 | "rawDecos": "!",
16 | "decos": [
17 | "!"
18 | ],
19 | "nodes": [
20 | {
21 | "type": "plain",
22 | "raw": "deco",
23 | "text": "deco"
24 | }
25 | ]
26 | },
27 | {
28 | "type": "plain",
29 | "raw": "]]",
30 | "text": "]]"
31 | }
32 | ]
33 | }
34 | ]
35 | `;
36 |
37 | exports[`strong > Simple strong 1`] = `
38 | [
39 | {
40 | "indent": 0,
41 | "type": "line",
42 | "nodes": [
43 | {
44 | "type": "strong",
45 | "raw": "[[Simple strong]]",
46 | "nodes": [
47 | {
48 | "type": "plain",
49 | "raw": "Simple strong",
50 | "text": "Simple strong"
51 | }
52 | ]
53 | }
54 | ]
55 | }
56 | ]
57 | `;
58 |
59 | exports[`strong > [[]] is not strong 1`] = `
60 | [
61 | {
62 | "indent": 0,
63 | "type": "line",
64 | "nodes": [
65 | {
66 | "type": "plain",
67 | "raw": "[[]]",
68 | "text": "[[]]"
69 | }
70 | ]
71 | }
72 | ]
73 | `;
74 |
--------------------------------------------------------------------------------
/test/line/strongIcon.test.ts:
--------------------------------------------------------------------------------
1 | import { deepStrictEqual } from "node:assert/strict";
2 | import { describe, it } from "node:test";
3 | import { parse } from "../../src/index.ts";
4 |
5 | describe("strongIcon", () => {
6 | it("Simple root strong icon", ({ assert }) => {
7 | assert.snapshot(
8 | parse("[[/icons/+1.icon]]", {
9 | hasTitle: false,
10 | }),
11 | );
12 | });
13 |
14 | it("Simple relative strong icon", ({ assert }) => {
15 | assert.snapshot(parse("[[me.icon]]", { hasTitle: false }));
16 | });
17 |
18 | it("Multiple icons", ({ assert }) => {
19 | assert.snapshot(parse("[[me.icon*3]]", { hasTitle: false }));
20 | });
21 |
22 | it("Strong icon and internal link on same line", ({ assert }) => {
23 | assert.snapshot(
24 | parse("[Internal link][[me.icon]]", {
25 | hasTitle: false,
26 | }),
27 | );
28 | });
29 |
30 | it("Each multiple strong icon must be different Object", ({ assert }) => {
31 | const [block] = parse("[[me.icon*2]]", { hasTitle: false });
32 | if (block === undefined || block.type !== "line") {
33 | throw new Error("fail");
34 | }
35 |
36 | assert.equal(block.nodes.length, 2);
37 | deepStrictEqual(block.nodes[0], block.nodes[1]);
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/test/line/strongIcon.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`strongIcon > Multiple icons 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "path": "me",
9 | "pathType": "relative",
10 | "type": "strongIcon",
11 | "raw": "[[me.icon*3]]"
12 | },
13 | {
14 | "path": "me",
15 | "pathType": "relative",
16 | "type": "strongIcon",
17 | "raw": "[[me.icon*3]]"
18 | },
19 | {
20 | "path": "me",
21 | "pathType": "relative",
22 | "type": "strongIcon",
23 | "raw": "[[me.icon*3]]"
24 | }
25 | ]
26 | }
27 | ]
28 | `;
29 |
30 | exports[`strongIcon > Simple relative strong icon 1`] = `
31 | [
32 | {
33 | "indent": 0,
34 | "type": "line",
35 | "nodes": [
36 | {
37 | "path": "me",
38 | "pathType": "relative",
39 | "type": "strongIcon",
40 | "raw": "[[me.icon]]"
41 | }
42 | ]
43 | }
44 | ]
45 | `;
46 |
47 | exports[`strongIcon > Simple root strong icon 1`] = `
48 | [
49 | {
50 | "indent": 0,
51 | "type": "line",
52 | "nodes": [
53 | {
54 | "path": "/icons/+1",
55 | "pathType": "root",
56 | "type": "strongIcon",
57 | "raw": "[[/icons/+1.icon]]"
58 | }
59 | ]
60 | }
61 | ]
62 | `;
63 |
64 | exports[`strongIcon > Strong icon and internal link on same line 1`] = `
65 | [
66 | {
67 | "indent": 0,
68 | "type": "line",
69 | "nodes": [
70 | {
71 | "type": "link",
72 | "raw": "[Internal link]",
73 | "pathType": "relative",
74 | "href": "Internal link",
75 | "content": ""
76 | },
77 | {
78 | "path": "me",
79 | "pathType": "relative",
80 | "type": "strongIcon",
81 | "raw": "[[me.icon]]"
82 | }
83 | ]
84 | }
85 | ]
86 | `;
87 |
--------------------------------------------------------------------------------
/test/line/strongImage.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "node:test";
2 | import { parse } from "../../src/index.ts";
3 |
4 | describe("strongImage", () => {
5 | it("Simple strong image", ({ assert }) => {
6 | assert.snapshot(
7 | parse(
8 | `
9 | [[http://example.com/image.png]]
10 | [[https://example.com/image.JPG]]
11 | [[https://example.com/image.svg]]
12 | [[https://example.com/image.GIF]]
13 | [[https://example.com/image.webp]]
14 | `.trim(),
15 | {
16 | hasTitle: false,
17 | },
18 | ),
19 | );
20 | });
21 |
22 | it("HTTP jpeg strong image with special and japanese chars", ({ assert }) => {
23 | assert.snapshot(
24 | parse("[[http://example.com/~!@#$%^&*()_+`-={}\\'\"?,.<>|/画像.jpeg]]", {
25 | hasTitle: false,
26 | }),
27 | );
28 | });
29 |
30 | it("Gyazo image", ({ assert }) => {
31 | assert.snapshot(
32 | parse("[[https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815]]", {
33 | hasTitle: false,
34 | }),
35 | );
36 | });
37 |
38 | it("Direct Gyazo image", ({ assert }) => {
39 | assert.snapshot(
40 | parse("[[https://i.gyazo.com/0f82099330f378fe4917a1b4a5fe8815.png]]", {
41 | hasTitle: false,
42 | }),
43 | );
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/test/line/strongImage.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`strongImage > Direct Gyazo image 1`] = `
2 | [
3 | {
4 | "indent": 0,
5 | "type": "line",
6 | "nodes": [
7 | {
8 | "type": "strongImage",
9 | "raw": "[[https://i.gyazo.com/0f82099330f378fe4917a1b4a5fe8815.png]]",
10 | "src": "https://i.gyazo.com/0f82099330f378fe4917a1b4a5fe8815.png"
11 | }
12 | ]
13 | }
14 | ]
15 | `;
16 |
17 | exports[`strongImage > Gyazo image 1`] = `
18 | [
19 | {
20 | "indent": 0,
21 | "type": "line",
22 | "nodes": [
23 | {
24 | "type": "strongImage",
25 | "raw": "[[https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815]]",
26 | "src": "https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815/thumb/1000"
27 | }
28 | ]
29 | }
30 | ]
31 | `;
32 |
33 | exports[`strongImage > HTTP jpeg strong image with special and japanese chars 1`] = `
34 | [
35 | {
36 | "indent": 0,
37 | "type": "line",
38 | "nodes": [
39 | {
40 | "type": "strongImage",
41 | "raw": "[[http://example.com/~!@#$%^&*()_+\`-={}\\\\'\\"?,.<>|/画像.jpeg]]",
42 | "src": "http://example.com/~!@#$%^&*()_+\`-={}\\\\'\\"?,.<>|/画像.jpeg"
43 | }
44 | ]
45 | }
46 | ]
47 | `;
48 |
49 | exports[`strongImage > Simple strong image 1`] = `
50 | [
51 | {
52 | "indent": 0,
53 | "type": "line",
54 | "nodes": [
55 | {
56 | "type": "strongImage",
57 | "raw": "[[http://example.com/image.png]]",
58 | "src": "http://example.com/image.png"
59 | }
60 | ]
61 | },
62 | {
63 | "indent": 0,
64 | "type": "line",
65 | "nodes": [
66 | {
67 | "type": "strongImage",
68 | "raw": "[[https://example.com/image.JPG]]",
69 | "src": "https://example.com/image.JPG"
70 | }
71 | ]
72 | },
73 | {
74 | "indent": 0,
75 | "type": "line",
76 | "nodes": [
77 | {
78 | "type": "strongImage",
79 | "raw": "[[https://example.com/image.svg]]",
80 | "src": "https://example.com/image.svg"
81 | }
82 | ]
83 | },
84 | {
85 | "indent": 0,
86 | "type": "line",
87 | "nodes": [
88 | {
89 | "type": "strongImage",
90 | "raw": "[[https://example.com/image.GIF]]",
91 | "src": "https://example.com/image.GIF"
92 | }
93 | ]
94 | },
95 | {
96 | "indent": 0,
97 | "type": "line",
98 | "nodes": [
99 | {
100 | "type": "strongImage",
101 | "raw": "[[https://example.com/image.webp]]",
102 | "src": "https://example.com/image.webp"
103 | }
104 | ]
105 | }
106 | ]
107 | `;
108 |
--------------------------------------------------------------------------------
/test/page/index.test.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "node:fs";
2 | import path from "node:path";
3 | import { describe, it } from "node:test";
4 | import { parse } from "../../src/index.ts";
5 |
6 | describe("page", () => {
7 | it("Empty page", ({ assert }) => {
8 | const input = "";
9 | assert.snapshot(parse(input, { hasTitle: true }));
10 | });
11 |
12 | it("Title Block without `hasTitle` option", ({ assert }) => {
13 | const input = "Title";
14 | assert.snapshot(parse(input));
15 | });
16 |
17 | it("https://scrapbox.io/help/Syntax", ({ assert }) => {
18 | const input = fs
19 | .readFileSync(path.resolve(import.meta.dirname, "input.txt"))
20 | .toString();
21 | assert.snapshot(parse(input, { hasTitle: true }));
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/test/page/index.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`page > Empty page 1`] = `
2 | [
3 | {
4 | "type": "title",
5 | "text": ""
6 | }
7 | ]
8 | `;
9 |
10 | exports[`page > Title Block without \`hasTitle\` option 1`] = `
11 | [
12 | {
13 | "type": "title",
14 | "text": "Title"
15 | }
16 | ]
17 | `;
18 |
19 | exports[`page > https://scrapbox.io/help/Syntax 1`] = `
20 | [
21 | {
22 | "type": "title",
23 | "text": "Syntax"
24 | },
25 | {
26 | "indent": 0,
27 | "type": "line",
28 | "nodes": [
29 | {
30 | "type": "image",
31 | "raw": "[https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815]",
32 | "src": "https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815/thumb/1000",
33 | "link": ""
34 | }
35 | ]
36 | },
37 | {
38 | "indent": 0,
39 | "type": "line",
40 | "nodes": []
41 | },
42 | {
43 | "indent": 0,
44 | "type": "line",
45 | "nodes": []
46 | },
47 | {
48 | "indent": 0,
49 | "type": "line",
50 | "nodes": [
51 | {
52 | "type": "strong",
53 | "raw": "[[Internal Links]]",
54 | "nodes": [
55 | {
56 | "type": "plain",
57 | "raw": "Internal Links",
58 | "text": "Internal Links"
59 | }
60 | ]
61 | },
62 | {
63 | "type": "plain",
64 | "raw": " (linking to another page on scrapbox)",
65 | "text": " (linking to another page on scrapbox)"
66 | }
67 | ]
68 | },
69 | {
70 | "indent": 1,
71 | "type": "line",
72 | "nodes": [
73 | {
74 | "type": "code",
75 | "raw": "\`[link]\`",
76 | "text": "[link]"
77 | },
78 | {
79 | "type": "plain",
80 | "raw": " ⇒ ",
81 | "text": " ⇒ "
82 | },
83 | {
84 | "type": "link",
85 | "raw": "[Link]",
86 | "pathType": "relative",
87 | "href": "Link",
88 | "content": ""
89 | }
90 | ]
91 | },
92 | {
93 | "indent": 0,
94 | "type": "line",
95 | "nodes": []
96 | },
97 | {
98 | "indent": 0,
99 | "type": "line",
100 | "nodes": [
101 | {
102 | "type": "strong",
103 | "raw": "[[External Links]]",
104 | "nodes": [
105 | {
106 | "type": "plain",
107 | "raw": "External Links",
108 | "text": "External Links"
109 | }
110 | ]
111 | },
112 | {
113 | "type": "plain",
114 | "raw": " (linking to another web page)",
115 | "text": " (linking to another web page)"
116 | }
117 | ]
118 | },
119 | {
120 | "indent": 1,
121 | "type": "line",
122 | "nodes": [
123 | {
124 | "type": "code",
125 | "raw": "\`http://google.com\`",
126 | "text": "http://google.com"
127 | },
128 | {
129 | "type": "plain",
130 | "raw": " ⇒ ",
131 | "text": " ⇒ "
132 | },
133 | {
134 | "type": "link",
135 | "raw": "http://google.com",
136 | "pathType": "absolute",
137 | "href": "http://google.com",
138 | "content": ""
139 | }
140 | ]
141 | },
142 | {
143 | "indent": 1,
144 | "type": "line",
145 | "nodes": [
146 | {
147 | "type": "code",
148 | "raw": "\`[http://google.com Google]\`",
149 | "text": "[http://google.com Google]"
150 | },
151 | {
152 | "type": "plain",
153 | "raw": " ⇒ ",
154 | "text": " ⇒ "
155 | },
156 | {
157 | "type": "link",
158 | "raw": "[http://google.com Google]",
159 | "pathType": "absolute",
160 | "href": "http://google.com",
161 | "content": "Google"
162 | }
163 | ]
164 | },
165 | {
166 | "indent": 0,
167 | "type": "line",
168 | "nodes": [
169 | {
170 | "type": "plain",
171 | "raw": "or",
172 | "text": "or"
173 | }
174 | ]
175 | },
176 | {
177 | "indent": 1,
178 | "type": "line",
179 | "nodes": [
180 | {
181 | "type": "code",
182 | "raw": "\`[Google http://google.com]\`",
183 | "text": "[Google http://google.com]"
184 | },
185 | {
186 | "type": "plain",
187 | "raw": " ⇒ ",
188 | "text": " ⇒ "
189 | },
190 | {
191 | "type": "link",
192 | "raw": "[Google http://google.com]",
193 | "pathType": "absolute",
194 | "href": "http://google.com",
195 | "content": "Google"
196 | }
197 | ]
198 | },
199 | {
200 | "indent": 0,
201 | "type": "line",
202 | "nodes": []
203 | },
204 | {
205 | "indent": 0,
206 | "type": "line",
207 | "nodes": [
208 | {
209 | "type": "strong",
210 | "raw": "[[Images]]",
211 | "nodes": [
212 | {
213 | "type": "plain",
214 | "raw": "Images",
215 | "text": "Images"
216 | }
217 | ]
218 | }
219 | ]
220 | },
221 | {
222 | "indent": 1,
223 | "type": "line",
224 | "nodes": [
225 | {
226 | "type": "plain",
227 | "raw": "Direct image link ↓",
228 | "text": "Direct image link ↓"
229 | },
230 | {
231 | "type": "code",
232 | "raw": "\`[https://gyazo.com/da78df293f9e83a74b5402411e2f2e01.png]\`",
233 | "text": "[https://gyazo.com/da78df293f9e83a74b5402411e2f2e01.png]"
234 | }
235 | ]
236 | },
237 | {
238 | "indent": 1,
239 | "type": "line",
240 | "nodes": [
241 | {
242 | "type": "image",
243 | "raw": "[https://i.gyazo.com/da78df293f9e83a74b5402411e2f2e01.png]",
244 | "src": "https://i.gyazo.com/da78df293f9e83a74b5402411e2f2e01.png",
245 | "link": ""
246 | }
247 | ]
248 | },
249 | {
250 | "indent": 0,
251 | "type": "line",
252 | "nodes": []
253 | },
254 | {
255 | "indent": 0,
256 | "type": "line",
257 | "nodes": [
258 | {
259 | "type": "strong",
260 | "raw": "[[Clickable Thumbnail Links]]",
261 | "nodes": [
262 | {
263 | "type": "plain",
264 | "raw": "Clickable Thumbnail Links",
265 | "text": "Clickable Thumbnail Links"
266 | }
267 | ]
268 | }
269 | ]
270 | },
271 | {
272 | "indent": 1,
273 | "type": "line",
274 | "nodes": [
275 | {
276 | "type": "plain",
277 | "raw": "↓ ",
278 | "text": "↓ "
279 | },
280 | {
281 | "type": "code",
282 | "raw": "\`[http://cutedog.com https://i.gyazo.com/da78df293f9e83a74b5402411e2f2e01.png]\`",
283 | "text": "[http://cutedog.com https://i.gyazo.com/da78df293f9e83a74b5402411e2f2e01.png]"
284 | },
285 | {
286 | "type": "plain",
287 | "raw": " ",
288 | "text": " "
289 | }
290 | ]
291 | },
292 | {
293 | "indent": 1,
294 | "type": "line",
295 | "nodes": [
296 | {
297 | "type": "image",
298 | "raw": "[http://cutedog.com https://i.gyazo.com/da78df293f9e83a74b5402411e2f2e01.png]",
299 | "src": "https://i.gyazo.com/da78df293f9e83a74b5402411e2f2e01.png",
300 | "link": "http://cutedog.com"
301 | }
302 | ]
303 | },
304 | {
305 | "indent": 1,
306 | "type": "line",
307 | "nodes": [
308 | {
309 | "type": "plain",
310 | "raw": "Adding the link at the end also works, as before:",
311 | "text": "Adding the link at the end also works, as before:"
312 | }
313 | ]
314 | },
315 | {
316 | "indent": 2,
317 | "type": "line",
318 | "nodes": [
319 | {
320 | "type": "code",
321 | "raw": "\`[https://i.gyazo.com/da78df293f9e83a74b5402411e2f2e01.png http://cutedog.com]\`",
322 | "text": "[https://i.gyazo.com/da78df293f9e83a74b5402411e2f2e01.png http://cutedog.com]"
323 | }
324 | ]
325 | },
326 | {
327 | "indent": 0,
328 | "type": "line",
329 | "nodes": []
330 | },
331 | {
332 | "indent": 0,
333 | "type": "line",
334 | "nodes": [
335 | {
336 | "type": "strong",
337 | "raw": "[[Linking to other scrapbox projects]]",
338 | "nodes": [
339 | {
340 | "type": "plain",
341 | "raw": "Linking to other scrapbox projects",
342 | "text": "Linking to other scrapbox projects"
343 | }
344 | ]
345 | }
346 | ]
347 | },
348 | {
349 | "indent": 1,
350 | "type": "line",
351 | "nodes": [
352 | {
353 | "type": "code",
354 | "raw": "\`[/projectname/pagename]\`",
355 | "text": "[/projectname/pagename]"
356 | },
357 | {
358 | "type": "plain",
359 | "raw": " ⇛ ",
360 | "text": " ⇛ "
361 | },
362 | {
363 | "type": "link",
364 | "raw": "[/icons/check]",
365 | "pathType": "root",
366 | "href": "/icons/check",
367 | "content": ""
368 | }
369 | ]
370 | },
371 | {
372 | "indent": 1,
373 | "type": "line",
374 | "nodes": [
375 | {
376 | "type": "code",
377 | "raw": "\`[/projectname]\`",
378 | "text": "[/projectname]"
379 | },
380 | {
381 | "type": "plain",
382 | "raw": " ⇛ ",
383 | "text": " ⇛ "
384 | },
385 | {
386 | "type": "link",
387 | "raw": "[/icons]",
388 | "pathType": "root",
389 | "href": "/icons",
390 | "content": ""
391 | }
392 | ]
393 | },
394 | {
395 | "indent": 0,
396 | "type": "line",
397 | "nodes": []
398 | },
399 | {
400 | "indent": 0,
401 | "type": "line",
402 | "nodes": [
403 | {
404 | "type": "strong",
405 | "raw": "[[Icons]]",
406 | "nodes": [
407 | {
408 | "type": "plain",
409 | "raw": "Icons",
410 | "text": "Icons"
411 | }
412 | ]
413 | }
414 | ]
415 | },
416 | {
417 | "indent": 1,
418 | "type": "line",
419 | "nodes": [
420 | {
421 | "type": "code",
422 | "raw": "\`[ben.icon]\`",
423 | "text": "[ben.icon]"
424 | },
425 | {
426 | "type": "plain",
427 | "raw": " ⇛ ",
428 | "text": " ⇛ "
429 | },
430 | {
431 | "path": "ben",
432 | "pathType": "relative",
433 | "type": "icon",
434 | "raw": "[ben.icon]"
435 | }
436 | ]
437 | },
438 | {
439 | "indent": 1,
440 | "type": "line",
441 | "nodes": [
442 | {
443 | "type": "code",
444 | "raw": "\`[/icons/todo.icon]\`",
445 | "text": "[/icons/todo.icon]"
446 | },
447 | {
448 | "type": "plain",
449 | "raw": " ⇛ ",
450 | "text": " ⇛ "
451 | },
452 | {
453 | "path": "/icons/todo",
454 | "pathType": "root",
455 | "type": "icon",
456 | "raw": "[/icons/todo.icon]"
457 | }
458 | ]
459 | },
460 | {
461 | "indent": 0,
462 | "type": "line",
463 | "nodes": []
464 | },
465 | {
466 | "indent": 0,
467 | "type": "line",
468 | "nodes": [
469 | {
470 | "type": "strong",
471 | "raw": "[[Bold text]]",
472 | "nodes": [
473 | {
474 | "type": "plain",
475 | "raw": "Bold text",
476 | "text": "Bold text"
477 | }
478 | ]
479 | }
480 | ]
481 | },
482 | {
483 | "indent": 1,
484 | "type": "line",
485 | "nodes": [
486 | {
487 | "type": "code",
488 | "raw": "\`[[Bold]]\`",
489 | "text": "[[Bold]]"
490 | },
491 | {
492 | "type": "plain",
493 | "raw": " or ",
494 | "text": " or "
495 | },
496 | {
497 | "type": "code",
498 | "raw": "\`[* Bold]\`",
499 | "text": "[* Bold]"
500 | },
501 | {
502 | "type": "plain",
503 | "raw": "⇒ ",
504 | "text": "⇒ "
505 | },
506 | {
507 | "type": "strong",
508 | "raw": "[[Bold]]",
509 | "nodes": [
510 | {
511 | "type": "plain",
512 | "raw": "Bold",
513 | "text": "Bold"
514 | }
515 | ]
516 | }
517 | ]
518 | },
519 | {
520 | "indent": 0,
521 | "type": "line",
522 | "nodes": []
523 | },
524 | {
525 | "indent": 0,
526 | "type": "line",
527 | "nodes": [
528 | {
529 | "type": "strong",
530 | "raw": "[[Italic text]]",
531 | "nodes": [
532 | {
533 | "type": "plain",
534 | "raw": "Italic text",
535 | "text": "Italic text"
536 | }
537 | ]
538 | }
539 | ]
540 | },
541 | {
542 | "indent": 1,
543 | "type": "line",
544 | "nodes": [
545 | {
546 | "type": "code",
547 | "raw": "\`[/ italic]\`",
548 | "text": "[/ italic]"
549 | },
550 | {
551 | "type": "plain",
552 | "raw": "⇛ ",
553 | "text": "⇛ "
554 | },
555 | {
556 | "type": "decoration",
557 | "raw": "[/ italic]",
558 | "rawDecos": "/",
559 | "decos": [
560 | "/"
561 | ],
562 | "nodes": [
563 | {
564 | "type": "plain",
565 | "raw": "italic",
566 | "text": "italic"
567 | }
568 | ]
569 | }
570 | ]
571 | },
572 | {
573 | "indent": 0,
574 | "type": "line",
575 | "nodes": []
576 | },
577 | {
578 | "indent": 0,
579 | "type": "line",
580 | "nodes": [
581 | {
582 | "type": "strong",
583 | "raw": "[[ Strikethrough text]]",
584 | "nodes": [
585 | {
586 | "type": "plain",
587 | "raw": " Strikethrough text",
588 | "text": " Strikethrough text"
589 | }
590 | ]
591 | }
592 | ]
593 | },
594 | {
595 | "indent": 1,
596 | "type": "line",
597 | "nodes": [
598 | {
599 | "type": "code",
600 | "raw": "\`[- strikethrough]\`",
601 | "text": "[- strikethrough]"
602 | },
603 | {
604 | "type": "plain",
605 | "raw": "⇛ ",
606 | "text": "⇛ "
607 | },
608 | {
609 | "type": "decoration",
610 | "raw": "[- strikethrough]",
611 | "rawDecos": "-",
612 | "decos": [
613 | "-"
614 | ],
615 | "nodes": [
616 | {
617 | "type": "plain",
618 | "raw": "strikethrough",
619 | "text": "strikethrough"
620 | }
621 | ]
622 | }
623 | ]
624 | },
625 | {
626 | "indent": 0,
627 | "type": "line",
628 | "nodes": [
629 | {
630 | "type": "image",
631 | "raw": "[https://gyazo.com/00ab07461d502db91c8ae170276d1396]",
632 | "src": "https://gyazo.com/00ab07461d502db91c8ae170276d1396/thumb/1000",
633 | "link": ""
634 | }
635 | ]
636 | },
637 | {
638 | "indent": 0,
639 | "type": "line",
640 | "nodes": []
641 | },
642 | {
643 | "indent": 0,
644 | "type": "line",
645 | "nodes": [
646 | {
647 | "type": "strong",
648 | "raw": "[[Bullet points]]",
649 | "nodes": [
650 | {
651 | "type": "plain",
652 | "raw": "Bullet points",
653 | "text": "Bullet points"
654 | }
655 | ]
656 | }
657 | ]
658 | },
659 | {
660 | "indent": 1,
661 | "type": "line",
662 | "nodes": [
663 | {
664 | "type": "plain",
665 | "raw": "Press space or tab on a new line to indent and create a bullet point",
666 | "text": "Press space or tab on a new line to indent and create a bullet point"
667 | }
668 | ]
669 | },
670 | {
671 | "indent": 2,
672 | "type": "line",
673 | "nodes": [
674 | {
675 | "type": "plain",
676 | "raw": "Press backspace to remove the indent / bullet point",
677 | "text": "Press backspace to remove the indent / bullet point"
678 | }
679 | ]
680 | },
681 | {
682 | "indent": 0,
683 | "type": "line",
684 | "nodes": []
685 | },
686 | {
687 | "indent": 0,
688 | "type": "line",
689 | "nodes": [
690 | {
691 | "type": "strong",
692 | "raw": "[[Internal links serve triple duty]]",
693 | "nodes": [
694 | {
695 | "type": "plain",
696 | "raw": "Internal links serve triple duty",
697 | "text": "Internal links serve triple duty"
698 | }
699 | ]
700 | }
701 | ]
702 | },
703 | {
704 | "indent": 1,
705 | "type": "line",
706 | "nodes": [
707 | {
708 | "type": "code",
709 | "raw": "\`[Links]\`",
710 | "text": "[Links]"
711 | },
712 | {
713 | "type": "plain",
714 | "raw": " or ",
715 | "text": " or "
716 | },
717 | {
718 | "type": "code",
719 | "raw": "\`#links\`",
720 | "text": "#links"
721 | },
722 | {
723 | "type": "plain",
724 | "raw": " are two ways to make links. They do three things",
725 | "text": " are two ways to make links. They do three things"
726 | }
727 | ]
728 | },
729 | {
730 | "indent": 2,
731 | "type": "line",
732 | "nodes": [
733 | {
734 | "type": "plain",
735 | "raw": "An internal link to a page",
736 | "text": "An internal link to a page"
737 | }
738 | ]
739 | },
740 | {
741 | "indent": 2,
742 | "type": "line",
743 | "nodes": [
744 | {
745 | "type": "plain",
746 | "raw": "A ",
747 | "text": "A "
748 | },
749 | {
750 | "type": "link",
751 | "raw": "[Bi-directional link]",
752 | "pathType": "relative",
753 | "href": "Bi-directional link",
754 | "content": ""
755 | },
756 | {
757 | "type": "plain",
758 | "raw": " back to the source",
759 | "text": " back to the source"
760 | }
761 | ]
762 | },
763 | {
764 | "indent": 2,
765 | "type": "line",
766 | "nodes": [
767 | {
768 | "type": "plain",
769 | "raw": "A ",
770 | "text": "A "
771 | },
772 | {
773 | "type": "link",
774 | "raw": "[2-hop link]",
775 | "pathType": "relative",
776 | "href": "2-hop link",
777 | "content": ""
778 | },
779 | {
780 | "type": "plain",
781 | "raw": " so you can find more related pages",
782 | "text": " so you can find more related pages"
783 | }
784 | ]
785 | },
786 | {
787 | "indent": 0,
788 | "type": "line",
789 | "nodes": []
790 | },
791 | {
792 | "indent": 0,
793 | "type": "line",
794 | "nodes": [
795 | {
796 | "type": "strong",
797 | "raw": "[[Block quote]]",
798 | "nodes": [
799 | {
800 | "type": "plain",
801 | "raw": "Block quote",
802 | "text": "Block quote"
803 | }
804 | ]
805 | }
806 | ]
807 | },
808 | {
809 | "indent": 0,
810 | "type": "line",
811 | "nodes": [
812 | {
813 | "type": "quote",
814 | "raw": "> use the right caret \`>\` at the beginning of a line to get a block quote ",
815 | "nodes": [
816 | {
817 | "type": "plain",
818 | "raw": " use the right caret ",
819 | "text": " use the right caret "
820 | },
821 | {
822 | "type": "code",
823 | "raw": "\`>\`",
824 | "text": ">"
825 | },
826 | {
827 | "type": "plain",
828 | "raw": " at the beginning of a line to get a block quote ",
829 | "text": " at the beginning of a line to get a block quote "
830 | }
831 | ]
832 | }
833 | ]
834 | },
835 | {
836 | "indent": 0,
837 | "type": "line",
838 | "nodes": []
839 | },
840 | {
841 | "indent": 0,
842 | "type": "line",
843 | "nodes": [
844 | {
845 | "type": "decoration",
846 | "raw": "[* Mouse based styling]",
847 | "rawDecos": "*",
848 | "decos": [
849 | "*-1"
850 | ],
851 | "nodes": [
852 | {
853 | "type": "plain",
854 | "raw": "Mouse based styling",
855 | "text": "Mouse based styling"
856 | }
857 | ]
858 | }
859 | ]
860 | },
861 | {
862 | "indent": 0,
863 | "type": "line",
864 | "nodes": [
865 | {
866 | "type": "image",
867 | "raw": "[https://gyazo.com/a515ab169b1e371641f7e04bfa92adbc]",
868 | "src": "https://gyazo.com/a515ab169b1e371641f7e04bfa92adbc/thumb/1000",
869 | "link": ""
870 | }
871 | ]
872 | },
873 | {
874 | "indent": 0,
875 | "type": "line",
876 | "nodes": [
877 | {
878 | "type": "strong",
879 | "raw": "[[[Code notation]]]",
880 | "nodes": [
881 | {
882 | "type": "link",
883 | "raw": "[Code notation]",
884 | "pathType": "relative",
885 | "href": "Code notation",
886 | "content": ""
887 | }
888 | ]
889 | }
890 | ]
891 | },
892 | {
893 | "indent": 1,
894 | "type": "line",
895 | "nodes": [
896 | {
897 | "type": "plain",
898 | "raw": "Use backquotes or backticks, \`, to highlight code ",
899 | "text": "Use backquotes or backticks, \`, to highlight code "
900 | }
901 | ]
902 | },
903 | {
904 | "indent": 1,
905 | "type": "line",
906 | "nodes": [
907 | {
908 | "type": "plain",
909 | "raw": "e.g. ",
910 | "text": "e.g. "
911 | },
912 | {
913 | "type": "code",
914 | "raw": "\`function() { return true }\`",
915 | "text": "function() { return true }"
916 | }
917 | ]
918 | },
919 | {
920 | "indent": 0,
921 | "type": "line",
922 | "nodes": []
923 | },
924 | {
925 | "indent": 0,
926 | "type": "line",
927 | "nodes": [
928 | {
929 | "type": "strong",
930 | "raw": "[[[Code blocks]]]",
931 | "nodes": [
932 | {
933 | "type": "link",
934 | "raw": "[Code blocks]",
935 | "pathType": "relative",
936 | "href": "Code blocks",
937 | "content": ""
938 | }
939 | ]
940 | }
941 | ]
942 | },
943 | {
944 | "indent": 1,
945 | "type": "line",
946 | "nodes": [
947 | {
948 | "type": "plain",
949 | "raw": "Typing ",
950 | "text": "Typing "
951 | },
952 | {
953 | "type": "code",
954 | "raw": "\`code:filename.extension\`",
955 | "text": "code:filename.extension"
956 | },
957 | {
958 | "type": "plain",
959 | "raw": "or",
960 | "text": "or"
961 | },
962 | {
963 | "type": "code",
964 | "raw": "\`code:filename\`",
965 | "text": "code:filename"
966 | },
967 | {
968 | "type": "plain",
969 | "raw": "can be used to create a new code snippet and and display it as a block",
970 | "text": "can be used to create a new code snippet and and display it as a block"
971 | }
972 | ]
973 | },
974 | {
975 | "indent": 2,
976 | "type": "line",
977 | "nodes": [
978 | {
979 | "type": "plain",
980 | "raw": "Language names may be abbreviated",
981 | "text": "Language names may be abbreviated"
982 | }
983 | ]
984 | },
985 | {
986 | "indent": 1,
987 | "type": "codeBlock",
988 | "fileName": "hello.js",
989 | "content": "function () {\\n alert(document.location.href)\\n console.log(\\"hello\\")\\n // You can also write comments!\\n}"
990 | },
991 | {
992 | "indent": 0,
993 | "type": "line",
994 | "nodes": []
995 | },
996 | {
997 | "indent": 0,
998 | "type": "line",
999 | "nodes": [
1000 | {
1001 | "type": "strong",
1002 | "raw": "[[[Tables]]]",
1003 | "nodes": [
1004 | {
1005 | "type": "link",
1006 | "raw": "[Tables]",
1007 | "pathType": "relative",
1008 | "href": "Tables",
1009 | "content": ""
1010 | }
1011 | ]
1012 | }
1013 | ]
1014 | },
1015 | {
1016 | "indent": 1,
1017 | "type": "line",
1018 | "nodes": [
1019 | {
1020 | "type": "plain",
1021 | "raw": "Type table: tablename to create a table",
1022 | "text": "Type table: tablename to create a table"
1023 | }
1024 | ]
1025 | },
1026 | {
1027 | "indent": 1,
1028 | "type": "line",
1029 | "nodes": [
1030 | {
1031 | "type": "plain",
1032 | "raw": "Use tab to move to the next column, use enter to move to the next row.",
1033 | "text": "Use tab to move to the next column, use enter to move to the next row."
1034 | }
1035 | ]
1036 | },
1037 | {
1038 | "indent": 1,
1039 | "type": "line",
1040 | "nodes": [
1041 | {
1042 | "type": "plain",
1043 | "raw": "An example:",
1044 | "text": "An example:"
1045 | }
1046 | ]
1047 | },
1048 | {
1049 | "indent": 0,
1050 | "type": "table",
1051 | "fileName": "hello",
1052 | "cells": [
1053 | [
1054 | [
1055 | {
1056 | "type": "plain",
1057 | "raw": "1",
1058 | "text": "1"
1059 | }
1060 | ],
1061 | [
1062 | {
1063 | "type": "plain",
1064 | "raw": "2",
1065 | "text": "2"
1066 | }
1067 | ],
1068 | [
1069 | {
1070 | "type": "plain",
1071 | "raw": "3",
1072 | "text": "3"
1073 | }
1074 | ]
1075 | ],
1076 | [
1077 | [
1078 | {
1079 | "type": "plain",
1080 | "raw": "1 ",
1081 | "text": "1 "
1082 | }
1083 | ],
1084 | [
1085 | {
1086 | "type": "plain",
1087 | "raw": "2 ",
1088 | "text": "2 "
1089 | }
1090 | ],
1091 | [
1092 | {
1093 | "type": "plain",
1094 | "raw": "3",
1095 | "text": "3"
1096 | }
1097 | ]
1098 | ],
1099 | [
1100 | [
1101 | {
1102 | "type": "plain",
1103 | "raw": "------",
1104 | "text": "------"
1105 | }
1106 | ],
1107 | [
1108 | {
1109 | "type": "plain",
1110 | "raw": "------",
1111 | "text": "------"
1112 | }
1113 | ],
1114 | [
1115 | {
1116 | "type": "plain",
1117 | "raw": "------",
1118 | "text": "------"
1119 | }
1120 | ]
1121 | ],
1122 | [
1123 | [
1124 | {
1125 | "type": "plain",
1126 | "raw": "a",
1127 | "text": "a"
1128 | }
1129 | ],
1130 | [
1131 | {
1132 | "type": "plain",
1133 | "raw": "b",
1134 | "text": "b"
1135 | }
1136 | ],
1137 | [
1138 | {
1139 | "type": "plain",
1140 | "raw": "c",
1141 | "text": "c"
1142 | }
1143 | ]
1144 | ]
1145 | ]
1146 | },
1147 | {
1148 | "indent": 0,
1149 | "type": "line",
1150 | "nodes": []
1151 | },
1152 | {
1153 | "indent": 0,
1154 | "type": "line",
1155 | "nodes": []
1156 | },
1157 | {
1158 | "indent": 0,
1159 | "type": "line",
1160 | "nodes": [
1161 | {
1162 | "type": "decoration",
1163 | "raw": "[* [Mathematical notation]]",
1164 | "rawDecos": "*",
1165 | "decos": [
1166 | "*-1"
1167 | ],
1168 | "nodes": [
1169 | {
1170 | "type": "link",
1171 | "raw": "[Mathematical notation]",
1172 | "pathType": "relative",
1173 | "href": "Mathematical notation",
1174 | "content": ""
1175 | }
1176 | ]
1177 | }
1178 | ]
1179 | },
1180 | {
1181 | "indent": 1,
1182 | "type": "line",
1183 | "nodes": [
1184 | {
1185 | "type": "plain",
1186 | "raw": "Using ",
1187 | "text": "Using "
1188 | },
1189 | {
1190 | "type": "link",
1191 | "raw": "[TeX https://en.wikipedia.org/wiki/TeX]",
1192 | "pathType": "absolute",
1193 | "href": "https://en.wikipedia.org/wiki/TeX",
1194 | "content": "TeX"
1195 | },
1196 | {
1197 | "type": "plain",
1198 | "raw": " inside of brackets with a dollar sign ",
1199 | "text": " inside of brackets with a dollar sign "
1200 | },
1201 | {
1202 | "type": "code",
1203 | "raw": "\`[$ TeX here ]\`",
1204 | "text": "[$ TeX here ]"
1205 | },
1206 | {
1207 | "type": "plain",
1208 | "raw": ", you can format math or science formulas, like so: ",
1209 | "text": ", you can format math or science formulas, like so: "
1210 | },
1211 | {
1212 | "type": "formula",
1213 | "raw": "[$ E = mc^2]",
1214 | "formula": "E = mc^2"
1215 | }
1216 | ]
1217 | },
1218 | {
1219 | "indent": 0,
1220 | "type": "line",
1221 | "nodes": []
1222 | },
1223 | {
1224 | "indent": 0,
1225 | "type": "line",
1226 | "nodes": [
1227 | {
1228 | "type": "decoration",
1229 | "raw": "[* [Userscript]]",
1230 | "rawDecos": "*",
1231 | "decos": [
1232 | "*-1"
1233 | ],
1234 | "nodes": [
1235 | {
1236 | "type": "link",
1237 | "raw": "[Userscript]",
1238 | "pathType": "relative",
1239 | "href": "Userscript",
1240 | "content": ""
1241 | }
1242 | ]
1243 | }
1244 | ]
1245 | },
1246 | {
1247 | "indent": 1,
1248 | "type": "line",
1249 | "nodes": [
1250 | {
1251 | "type": "plain",
1252 | "raw": "You can even add javascript to customize Scrapbox to your liking.",
1253 | "text": "You can even add javascript to customize Scrapbox to your liking."
1254 | }
1255 | ]
1256 | },
1257 | {
1258 | "indent": 0,
1259 | "type": "line",
1260 | "nodes": []
1261 | }
1262 | ]
1263 | `;
1264 |
--------------------------------------------------------------------------------
/test/page/input.txt:
--------------------------------------------------------------------------------
1 | Syntax
2 | [https://gyazo.com/0f82099330f378fe4917a1b4a5fe8815]
3 |
4 |
5 | [[Internal Links]] (linking to another page on scrapbox)
6 | `[link]` ⇒ [Link]
7 |
8 | [[External Links]] (linking to another web page)
9 | `http://google.com` ⇒ http://google.com
10 | `[http://google.com Google]` ⇒ [http://google.com Google]
11 | or
12 | `[Google http://google.com]` ⇒ [Google http://google.com]
13 |
14 | [[Images]]
15 | Direct image link ↓`[https://gyazo.com/da78df293f9e83a74b5402411e2f2e01.png]`
16 | [https://i.gyazo.com/da78df293f9e83a74b5402411e2f2e01.png]
17 |
18 | [[Clickable Thumbnail Links]]
19 | ↓ `[http://cutedog.com https://i.gyazo.com/da78df293f9e83a74b5402411e2f2e01.png]`
20 | [http://cutedog.com https://i.gyazo.com/da78df293f9e83a74b5402411e2f2e01.png]
21 | Adding the link at the end also works, as before:
22 | `[https://i.gyazo.com/da78df293f9e83a74b5402411e2f2e01.png http://cutedog.com]`
23 |
24 | [[Linking to other scrapbox projects]]
25 | `[/projectname/pagename]` ⇛ [/icons/check]
26 | `[/projectname]` ⇛ [/icons]
27 |
28 | [[Icons]]
29 | `[ben.icon]` ⇛ [ben.icon]
30 | `[/icons/todo.icon]` ⇛ [/icons/todo.icon]
31 |
32 | [[Bold text]]
33 | `[[Bold]]` or `[* Bold]`⇒ [[Bold]]
34 |
35 | [[Italic text]]
36 | `[/ italic]`⇛ [/ italic]
37 |
38 | [[ Strikethrough text]]
39 | `[- strikethrough]`⇛ [- strikethrough]
40 | [https://gyazo.com/00ab07461d502db91c8ae170276d1396]
41 |
42 | [[Bullet points]]
43 | Press space or tab on a new line to indent and create a bullet point
44 | Press backspace to remove the indent / bullet point
45 |
46 | [[Internal links serve triple duty]]
47 | `[Links]` or `#links` are two ways to make links. They do three things
48 | An internal link to a page
49 | A [Bi-directional link] back to the source
50 | A [2-hop link] so you can find more related pages
51 |
52 | [[Block quote]]
53 | > use the right caret `>` at the beginning of a line to get a block quote
54 |
55 | [* Mouse based styling]
56 | [https://gyazo.com/a515ab169b1e371641f7e04bfa92adbc]
57 | [[[Code notation]]]
58 | Use backquotes or backticks, `, to highlight code
59 | e.g. `function() { return true }`
60 |
61 | [[[Code blocks]]]
62 | Typing `code:filename.extension`or`code:filename`can be used to create a new code snippet and and display it as a block
63 | Language names may be abbreviated
64 | code:hello.js
65 | function () {
66 | alert(document.location.href)
67 | console.log("hello")
68 | // You can also write comments!
69 | }
70 |
71 | [[[Tables]]]
72 | Type table: tablename to create a table
73 | Use tab to move to the next column, use enter to move to the next row.
74 | An example:
75 | table:hello
76 | 1 2 3
77 | 1 2 3
78 | ------ ------ ------
79 | a b c
80 |
81 |
82 | [* [Mathematical notation]]
83 | Using [TeX https://en.wikipedia.org/wiki/TeX] inside of brackets with a dollar sign `[$ TeX here ]`, you can format math or science formulas, like so: [$ E = mc^2]
84 |
85 | [* [Userscript]]
86 | You can even add javascript to customize Scrapbox to your liking.
87 |
--------------------------------------------------------------------------------
/test/table/index.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-tabs, no-irregular-whitespace */
2 | import { describe, it } from "node:test";
3 | import { parse } from "../../src/index.ts";
4 |
5 | describe("Table", () => {
6 | it("Simple table", ({ assert }) => {
7 | assert.snapshot(
8 | parse(
9 | `
10 | table:hello
11 | ${"\t"}1${"\t"}2${"\t"}3
12 | ${"\t"}1 ${"\t"}2 ${"\t"}3
13 | ${"\t"}------${"\t"}------${"\t"}------
14 | ${"\t"}a${"\t"}b${"\t"}c
15 | `.trim(),
16 | { hasTitle: false },
17 | ),
18 | );
19 | });
20 |
21 | it("Bulleted table", ({ assert }) => {
22 | assert.snapshot(
23 | parse(
24 | ` table:bulleted
25 | ${"\t"}1${"\t"}2${"\t"}3
26 | ${"\t"}1 ${"\t"}2 ${"\t"}3
27 | ${"\t"}------${"\t"}------${"\t"}------
28 | ${"\t"}a${"\t"}b${"\t"}c`,
29 | { hasTitle: false },
30 | ),
31 | );
32 | });
33 |
34 | it("Table with empty cells", ({ assert }) => {
35 | assert.snapshot(
36 | parse(
37 | `table:${" "}
38 | ${"\t"} ${"\t"} ${"\t"}${" "}
39 | ${"\t"}${"\t"}${"\t"}`,
40 | { hasTitle: false },
41 | ),
42 | );
43 | });
44 |
45 | it("Staggered table", ({ assert }) => {
46 | assert.snapshot(
47 | parse(
48 | `table:Staggered
49 | ${"\t"}1${"\t"}2${"\t"}3${"\t"}4
50 | ${"\t"}1${"\t"}2${"\t"}3
51 | ${"\t"}1
52 | ${"\t"}1${"\t"}2
53 | ${"\t"}`,
54 | { hasTitle: false },
55 | ),
56 | );
57 | });
58 |
59 | it("Consecutive table", ({ assert }) => {
60 | assert.snapshot(
61 | parse(
62 | `
63 | table:hello
64 | ${"\t"}1${"\t"}2${"\t"}3
65 | ${"\t"}1 ${"\t"}2 ${"\t"}3
66 | ${"\t"}------${"\t"}------${"\t"}------
67 | ${"\t"}a${"\t"}b${"\t"}c
68 | table:hello
69 | ${"\t"}1${"\t"}2${"\t"}3
70 | ${"\t"}1 ${"\t"}2 ${"\t"}3
71 | ${"\t"}------${"\t"}------${"\t"}------
72 | ${"\t"}a${"\t"}b${"\t"}c
73 | `.trim(),
74 | { hasTitle: false },
75 | ),
76 | );
77 | });
78 |
79 | it("Node in table cells", ({ assert }) => {
80 | assert.snapshot(
81 | parse(
82 | `
83 | table:node in table cells
84 | ${"\t"}#hashtag
85 | ${"\t"}[* deco]
86 | ${"\t"}[ ]
87 | ${"\t"}\`code\`
88 | ${"\t"}https://external.com
89 | ${"\t"}[https://external.com]
90 | ${"\t"}[left https://external.com]
91 | ${"\t"}[https://external.com right]
92 | ${"\t"}[$ x]
93 | ${"\t"}[N35.6812362,E139.7649361]
94 | ${"\t"}#hashTag
95 | ${"\t"}? helpfeel
96 | ${"\t"}$ commandLine
97 | ${"\t"}[progfay.icon]
98 | ${"\t"}[https://image.com/image.png]
99 | ${"\t"}[link]
100 | ${"\t"}plain
101 | ${"\t"}> quote
102 | ${"\t"}[[progfay.icon]]
103 | ${"\t"}[[https://image.com/image.png]]
104 | ${"\t"}[[strong]]
105 | `.trim(),
106 | { hasTitle: false },
107 | ),
108 | );
109 | });
110 | });
111 |
--------------------------------------------------------------------------------
/test/table/index.test.ts.snapshot:
--------------------------------------------------------------------------------
1 | exports[`Table > Bulleted table 1`] = `
2 | [
3 | {
4 | "indent": 1,
5 | "type": "table",
6 | "fileName": "bulleted",
7 | "cells": [
8 | [
9 | [
10 | {
11 | "type": "plain",
12 | "raw": "1",
13 | "text": "1"
14 | }
15 | ],
16 | [
17 | {
18 | "type": "plain",
19 | "raw": "2",
20 | "text": "2"
21 | }
22 | ],
23 | [
24 | {
25 | "type": "plain",
26 | "raw": "3",
27 | "text": "3"
28 | }
29 | ]
30 | ],
31 | [
32 | [
33 | {
34 | "type": "plain",
35 | "raw": "1 ",
36 | "text": "1 "
37 | }
38 | ],
39 | [
40 | {
41 | "type": "plain",
42 | "raw": "2 ",
43 | "text": "2 "
44 | }
45 | ],
46 | [
47 | {
48 | "type": "plain",
49 | "raw": "3",
50 | "text": "3"
51 | }
52 | ]
53 | ],
54 | [
55 | [
56 | {
57 | "type": "plain",
58 | "raw": "------",
59 | "text": "------"
60 | }
61 | ],
62 | [
63 | {
64 | "type": "plain",
65 | "raw": "------",
66 | "text": "------"
67 | }
68 | ],
69 | [
70 | {
71 | "type": "plain",
72 | "raw": "------",
73 | "text": "------"
74 | }
75 | ]
76 | ],
77 | [
78 | [
79 | {
80 | "type": "plain",
81 | "raw": "a",
82 | "text": "a"
83 | }
84 | ],
85 | [
86 | {
87 | "type": "plain",
88 | "raw": "b",
89 | "text": "b"
90 | }
91 | ],
92 | [
93 | {
94 | "type": "plain",
95 | "raw": "c",
96 | "text": "c"
97 | }
98 | ]
99 | ]
100 | ]
101 | }
102 | ]
103 | `;
104 |
105 | exports[`Table > Consecutive table 1`] = `
106 | [
107 | {
108 | "indent": 0,
109 | "type": "table",
110 | "fileName": "hello",
111 | "cells": [
112 | [
113 | [
114 | {
115 | "type": "plain",
116 | "raw": "1",
117 | "text": "1"
118 | }
119 | ],
120 | [
121 | {
122 | "type": "plain",
123 | "raw": "2",
124 | "text": "2"
125 | }
126 | ],
127 | [
128 | {
129 | "type": "plain",
130 | "raw": "3",
131 | "text": "3"
132 | }
133 | ]
134 | ],
135 | [
136 | [
137 | {
138 | "type": "plain",
139 | "raw": "1 ",
140 | "text": "1 "
141 | }
142 | ],
143 | [
144 | {
145 | "type": "plain",
146 | "raw": "2 ",
147 | "text": "2 "
148 | }
149 | ],
150 | [
151 | {
152 | "type": "plain",
153 | "raw": "3",
154 | "text": "3"
155 | }
156 | ]
157 | ],
158 | [
159 | [
160 | {
161 | "type": "plain",
162 | "raw": "------",
163 | "text": "------"
164 | }
165 | ],
166 | [
167 | {
168 | "type": "plain",
169 | "raw": "------",
170 | "text": "------"
171 | }
172 | ],
173 | [
174 | {
175 | "type": "plain",
176 | "raw": "------",
177 | "text": "------"
178 | }
179 | ]
180 | ],
181 | [
182 | [
183 | {
184 | "type": "plain",
185 | "raw": "a",
186 | "text": "a"
187 | }
188 | ],
189 | [
190 | {
191 | "type": "plain",
192 | "raw": "b",
193 | "text": "b"
194 | }
195 | ],
196 | [
197 | {
198 | "type": "plain",
199 | "raw": "c",
200 | "text": "c"
201 | }
202 | ]
203 | ]
204 | ]
205 | },
206 | {
207 | "indent": 0,
208 | "type": "table",
209 | "fileName": "hello",
210 | "cells": [
211 | [
212 | [
213 | {
214 | "type": "plain",
215 | "raw": "1",
216 | "text": "1"
217 | }
218 | ],
219 | [
220 | {
221 | "type": "plain",
222 | "raw": "2",
223 | "text": "2"
224 | }
225 | ],
226 | [
227 | {
228 | "type": "plain",
229 | "raw": "3",
230 | "text": "3"
231 | }
232 | ]
233 | ],
234 | [
235 | [
236 | {
237 | "type": "plain",
238 | "raw": "1 ",
239 | "text": "1 "
240 | }
241 | ],
242 | [
243 | {
244 | "type": "plain",
245 | "raw": "2 ",
246 | "text": "2 "
247 | }
248 | ],
249 | [
250 | {
251 | "type": "plain",
252 | "raw": "3",
253 | "text": "3"
254 | }
255 | ]
256 | ],
257 | [
258 | [
259 | {
260 | "type": "plain",
261 | "raw": "------",
262 | "text": "------"
263 | }
264 | ],
265 | [
266 | {
267 | "type": "plain",
268 | "raw": "------",
269 | "text": "------"
270 | }
271 | ],
272 | [
273 | {
274 | "type": "plain",
275 | "raw": "------",
276 | "text": "------"
277 | }
278 | ]
279 | ],
280 | [
281 | [
282 | {
283 | "type": "plain",
284 | "raw": "a",
285 | "text": "a"
286 | }
287 | ],
288 | [
289 | {
290 | "type": "plain",
291 | "raw": "b",
292 | "text": "b"
293 | }
294 | ],
295 | [
296 | {
297 | "type": "plain",
298 | "raw": "c",
299 | "text": "c"
300 | }
301 | ]
302 | ]
303 | ]
304 | }
305 | ]
306 | `;
307 |
308 | exports[`Table > Node in table cells 1`] = `
309 | [
310 | {
311 | "indent": 0,
312 | "type": "table",
313 | "fileName": "node in table cells",
314 | "cells": [
315 | [
316 | [
317 | {
318 | "type": "plain",
319 | "raw": "#hashtag",
320 | "text": "#hashtag"
321 | }
322 | ]
323 | ],
324 | [
325 | [
326 | {
327 | "type": "plain",
328 | "raw": "[* deco]",
329 | "text": "[* deco]"
330 | }
331 | ]
332 | ],
333 | [
334 | [
335 | {
336 | "type": "plain",
337 | "raw": "[ ]",
338 | "text": "[ ]"
339 | }
340 | ]
341 | ],
342 | [
343 | [
344 | {
345 | "type": "plain",
346 | "raw": "\`code\`",
347 | "text": "\`code\`"
348 | }
349 | ]
350 | ],
351 | [
352 | [
353 | {
354 | "type": "plain",
355 | "raw": "https://external.com",
356 | "text": "https://external.com"
357 | }
358 | ]
359 | ],
360 | [
361 | [
362 | {
363 | "type": "plain",
364 | "raw": "[https://external.com]",
365 | "text": "[https://external.com]"
366 | }
367 | ]
368 | ],
369 | [
370 | [
371 | {
372 | "type": "plain",
373 | "raw": "[left https://external.com]",
374 | "text": "[left https://external.com]"
375 | }
376 | ]
377 | ],
378 | [
379 | [
380 | {
381 | "type": "plain",
382 | "raw": "[https://external.com right]",
383 | "text": "[https://external.com right]"
384 | }
385 | ]
386 | ],
387 | [
388 | [
389 | {
390 | "type": "plain",
391 | "raw": "[$ x]",
392 | "text": "[$ x]"
393 | }
394 | ]
395 | ],
396 | [
397 | [
398 | {
399 | "type": "plain",
400 | "raw": "[N35.6812362,E139.7649361]",
401 | "text": "[N35.6812362,E139.7649361]"
402 | }
403 | ]
404 | ],
405 | [
406 | [
407 | {
408 | "type": "plain",
409 | "raw": "#hashTag",
410 | "text": "#hashTag"
411 | }
412 | ]
413 | ],
414 | [
415 | [
416 | {
417 | "type": "plain",
418 | "raw": "? helpfeel",
419 | "text": "? helpfeel"
420 | }
421 | ]
422 | ],
423 | [
424 | [
425 | {
426 | "type": "plain",
427 | "raw": "$ commandLine",
428 | "text": "$ commandLine"
429 | }
430 | ]
431 | ],
432 | [
433 | [
434 | {
435 | "path": "progfay",
436 | "pathType": "relative",
437 | "type": "icon",
438 | "raw": "[progfay.icon]"
439 | }
440 | ]
441 | ],
442 | [
443 | [
444 | {
445 | "type": "plain",
446 | "raw": "[https://image.com/image.png]",
447 | "text": "[https://image.com/image.png]"
448 | }
449 | ]
450 | ],
451 | [
452 | [
453 | {
454 | "type": "link",
455 | "raw": "[link]",
456 | "pathType": "relative",
457 | "href": "link",
458 | "content": ""
459 | }
460 | ]
461 | ],
462 | [
463 | [
464 | {
465 | "type": "plain",
466 | "raw": "plain",
467 | "text": "plain"
468 | }
469 | ]
470 | ],
471 | [
472 | [
473 | {
474 | "type": "plain",
475 | "raw": "> quote",
476 | "text": "> quote"
477 | }
478 | ]
479 | ],
480 | [
481 | [
482 | {
483 | "type": "plain",
484 | "raw": "[[progfay.icon]]",
485 | "text": "[[progfay.icon]]"
486 | }
487 | ]
488 | ],
489 | [
490 | [
491 | {
492 | "type": "plain",
493 | "raw": "[[https://image.com/image.png]]",
494 | "text": "[[https://image.com/image.png]]"
495 | }
496 | ]
497 | ],
498 | [
499 | [
500 | {
501 | "type": "plain",
502 | "raw": "[[strong]]",
503 | "text": "[[strong]]"
504 | }
505 | ]
506 | ]
507 | ]
508 | }
509 | ]
510 | `;
511 |
512 | exports[`Table > Simple table 1`] = `
513 | [
514 | {
515 | "indent": 0,
516 | "type": "table",
517 | "fileName": "hello",
518 | "cells": [
519 | [
520 | [
521 | {
522 | "type": "plain",
523 | "raw": "1",
524 | "text": "1"
525 | }
526 | ],
527 | [
528 | {
529 | "type": "plain",
530 | "raw": "2",
531 | "text": "2"
532 | }
533 | ],
534 | [
535 | {
536 | "type": "plain",
537 | "raw": "3",
538 | "text": "3"
539 | }
540 | ]
541 | ],
542 | [
543 | [
544 | {
545 | "type": "plain",
546 | "raw": "1 ",
547 | "text": "1 "
548 | }
549 | ],
550 | [
551 | {
552 | "type": "plain",
553 | "raw": "2 ",
554 | "text": "2 "
555 | }
556 | ],
557 | [
558 | {
559 | "type": "plain",
560 | "raw": "3",
561 | "text": "3"
562 | }
563 | ]
564 | ],
565 | [
566 | [
567 | {
568 | "type": "plain",
569 | "raw": "------",
570 | "text": "------"
571 | }
572 | ],
573 | [
574 | {
575 | "type": "plain",
576 | "raw": "------",
577 | "text": "------"
578 | }
579 | ],
580 | [
581 | {
582 | "type": "plain",
583 | "raw": "------",
584 | "text": "------"
585 | }
586 | ]
587 | ],
588 | [
589 | [
590 | {
591 | "type": "plain",
592 | "raw": "a",
593 | "text": "a"
594 | }
595 | ],
596 | [
597 | {
598 | "type": "plain",
599 | "raw": "b",
600 | "text": "b"
601 | }
602 | ],
603 | [
604 | {
605 | "type": "plain",
606 | "raw": "c",
607 | "text": "c"
608 | }
609 | ]
610 | ]
611 | ]
612 | }
613 | ]
614 | `;
615 |
616 | exports[`Table > Staggered table 1`] = `
617 | [
618 | {
619 | "indent": 0,
620 | "type": "table",
621 | "fileName": "Staggered",
622 | "cells": [
623 | [
624 | [
625 | {
626 | "type": "plain",
627 | "raw": "1",
628 | "text": "1"
629 | }
630 | ],
631 | [
632 | {
633 | "type": "plain",
634 | "raw": "2",
635 | "text": "2"
636 | }
637 | ],
638 | [
639 | {
640 | "type": "plain",
641 | "raw": "3",
642 | "text": "3"
643 | }
644 | ],
645 | [
646 | {
647 | "type": "plain",
648 | "raw": "4",
649 | "text": "4"
650 | }
651 | ]
652 | ],
653 | [
654 | [
655 | {
656 | "type": "plain",
657 | "raw": "1",
658 | "text": "1"
659 | }
660 | ],
661 | [
662 | {
663 | "type": "plain",
664 | "raw": "2",
665 | "text": "2"
666 | }
667 | ],
668 | [
669 | {
670 | "type": "plain",
671 | "raw": "3",
672 | "text": "3"
673 | }
674 | ]
675 | ],
676 | [
677 | [
678 | {
679 | "type": "plain",
680 | "raw": "1",
681 | "text": "1"
682 | }
683 | ]
684 | ],
685 | [
686 | [
687 | {
688 | "type": "plain",
689 | "raw": "1",
690 | "text": "1"
691 | }
692 | ],
693 | [
694 | {
695 | "type": "plain",
696 | "raw": "2",
697 | "text": "2"
698 | }
699 | ]
700 | ],
701 | [
702 | []
703 | ]
704 | ]
705 | }
706 | ]
707 | `;
708 |
709 | exports[`Table > Table with empty cells 1`] = `
710 | [
711 | {
712 | "indent": 0,
713 | "type": "table",
714 | "fileName": " ",
715 | "cells": [
716 | [
717 | [
718 | {
719 | "type": "plain",
720 | "raw": " ",
721 | "text": " "
722 | }
723 | ],
724 | [
725 | {
726 | "type": "plain",
727 | "raw": " ",
728 | "text": " "
729 | }
730 | ],
731 | [
732 | {
733 | "type": "plain",
734 | "raw": " ",
735 | "text": " "
736 | }
737 | ]
738 | ],
739 | [
740 | [],
741 | [],
742 | []
743 | ]
744 | ]
745 | }
746 | ]
747 | `;
748 |
--------------------------------------------------------------------------------
/test/title/index.test.ts:
--------------------------------------------------------------------------------
1 | import assert from "node:assert/strict";
2 | import { describe, it } from "node:test";
3 | import { getTitle } from "../../src/index.ts";
4 |
5 | describe("title", () => {
6 | it("Get title from simple page", () => {
7 | const title = getTitle("title\nline\nline\n");
8 | assert.strictEqual(title, "title");
9 | });
10 |
11 | it("Get title from empty page", () => {
12 | assert.strictEqual(getTitle(""), "Untitled");
13 | assert.strictEqual(getTitle(" \t"), "Untitled");
14 | assert.strictEqual(getTitle("\n"), "Untitled");
15 | assert.strictEqual(getTitle("\n \t"), "Untitled");
16 | });
17 |
18 | it("Get title from title only page", () => {
19 | const title = getTitle("title");
20 | assert.strictEqual(title, "title");
21 | });
22 |
23 | it("Get title from huge page", () => {
24 | const title = getTitle(`${" \n".repeat(10 ** 8)}title`);
25 | assert.strictEqual(title, "title");
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "es2018" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "Preserve" /* Specify what module code is generated. */,
29 | // "rootDir": "./", /* Specify the root folder within your source files. */
30 | "moduleResolution": "Bundler" /* Specify how TypeScript looks up a file from a given module specifier. */,
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | "allowImportingTsExtensions": true /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */,
39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
42 | // "resolveJsonModule": true, /* Enable importing .json files. */
43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
45 |
46 | /* JavaScript Support */
47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
50 |
51 | /* Emit */
52 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
53 | "declarationMap": true /* Create sourcemaps for d.ts files. */,
54 | // "emitDeclarationOnly": true /* Only output d.ts files and not JavaScript files. */,
55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
58 | // "outDir": "./", /* Specify an output folder for all emitted files. */
59 | "removeComments": true /* Disable emitting comments. */,
60 | "noEmit": true /* Disable emitting files from a compilation. */,
61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
62 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
63 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
64 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
65 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
66 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
67 | // "newLine": "crlf", /* Set the newline character for emitting files. */
68 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
69 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
70 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
71 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
72 | // "declarationDir": "./dist" /* Specify the output directory for generated declaration files. */,
73 |
74 | /* Interop Constraints */
75 | "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
76 | "verbatimModuleSyntax": true /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */,
77 | "isolatedDeclarations": true /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */,
78 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
79 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
80 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
81 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
82 |
83 | /* Type Checking */
84 | "strict": true /* Enable all strict type-checking options. */,
85 | "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
86 | "strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */,
87 | "strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */,
88 | "strictBindCallApply": true /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */,
89 | "strictPropertyInitialization": true /* Check for class properties that are declared but not set in the constructor. */,
90 | "noImplicitThis": true /* Enable error reporting when 'this' is given the type 'any'. */,
91 | "useUnknownInCatchVariables": true /* Default catch clause variables as 'unknown' instead of 'any'. */,
92 | "alwaysStrict": true /* Ensure 'use strict' is always emitted. */,
93 | "noUnusedLocals": true /* Enable error reporting when local variables aren't read. */,
94 | "noUnusedParameters": true /* Raise an error when a function parameter isn't read. */,
95 | "exactOptionalPropertyTypes": true /* Interpret optional property types as written, rather than adding 'undefined'. */,
96 | "noImplicitReturns": true /* Enable error reporting for codepaths that do not explicitly return in a function. */,
97 | "noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */,
98 | "noUncheckedIndexedAccess": true /* Add 'undefined' to a type when accessed using an index. */,
99 | "noImplicitOverride": true /* Ensure overriding members in derived classes are marked with an override modifier. */,
100 | "noPropertyAccessFromIndexSignature": true /* Enforces using indexed accessors for keys declared using an indexed type. */,
101 | "allowUnusedLabels": true /* Disable error reporting for unused labels. */,
102 | "allowUnreachableCode": true /* Disable error reporting for unreachable code. */,
103 |
104 | /* Completeness */
105 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
106 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
107 | },
108 | "include": ["src/**/*"]
109 | }
110 |
--------------------------------------------------------------------------------
/tsconfig.lint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig",
3 | "compilerOptions": {
4 | "emitDeclarationOnly": false,
5 | "noEmit": true,
6 | "isolatedDeclarations": false
7 | },
8 | "include": ["**/*"]
9 | }
10 |
--------------------------------------------------------------------------------
/tsdown.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from "node:path";
2 | import { defineConfig } from "tsdown";
3 |
4 | export default defineConfig([
5 | {
6 | entry: resolve(import.meta.dirname, "src/index.ts"),
7 | dts: true,
8 | minify: true,
9 | sourcemap: true,
10 | platform: "neutral",
11 | clean: true,
12 | },
13 | ]);
14 |
--------------------------------------------------------------------------------