├── .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 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge&color=AC1500&labelColor=222222)](LICENSE) 4 | [![npm version](https://img.shields.io/npm/v/@progfay/scrapbox-parser?style=for-the-badge&message=NPM&color=CB3837&logo=NPM&labelColor=222222&label=npm)](https://www.npmjs.com/package/@progfay/scrapbox-parser) 5 | [![Maintainability](https://img.shields.io/codeclimate/maintainability/progfay/scrapbox-parser?style=for-the-badge&message=Code+Climate&labelColor=222222&logo=Code+Climate&logoColor=FFFFFF)](https://codeclimate.com/github/progfay/scrapbox-parser/maintainability) 6 | [![Test Coverage](https://img.shields.io/codeclimate/coverage/progfay/scrapbox-parser?style=for-the-badge&message=Code+Climate&labelColor=222222&logo=Code+Climate&logoColor=FFFFFF)](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 |
91 | 92 | 93 |
94 |
95 |
96 |
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 | --------------------------------------------------------------------------------