├── .github
├── FUNDING.yml
└── workflows
│ ├── codeql-analysis.yml
│ ├── deploy.yml
│ └── test.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.js
├── .yarnclean
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── package.json
├── packages
├── core
│ ├── package.json
│ └── src
│ │ ├── index.ts
│ │ └── lint.ts
├── scrapbox-lint
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ └── worker.ts
│ ├── tsconfig.json
│ └── webpack.config.js
└── website
│ ├── package.json
│ ├── resources
│ ├── 8737dd05a68d04d808dfdb81c6783be1.png
│ ├── browserCheck.js
│ ├── dict
│ │ ├── base.dat
│ │ ├── cc.dat
│ │ ├── check.dat
│ │ ├── tid.dat
│ │ ├── tid_map.dat
│ │ ├── tid_pos.dat
│ │ ├── unk.dat
│ │ ├── unk_char.dat
│ │ ├── unk_compat.dat
│ │ ├── unk_invoke.dat
│ │ ├── unk_map.dat
│ │ └── unk_pos.dat
│ ├── favicon.png
│ ├── googleAnalytics.js
│ ├── index.html
│ ├── j260_12_0.svg
│ └── manifest.json
│ ├── src
│ ├── App.tsx
│ ├── Edit
│ │ ├── Edit.tsx
│ │ ├── SettingDialog.tsx
│ │ ├── TextContainer
│ │ │ ├── PinIcon.tsx
│ │ │ ├── TextContainer.tsx
│ │ │ └── index.tsx
│ │ └── index.ts
│ ├── Empty.tsx
│ ├── Sidebar.tsx
│ ├── ThemeProvider.tsx
│ ├── browserCheck.ts
│ ├── index.tsx
│ ├── lintWorker.ts
│ ├── supportedBrowsers.json
│ ├── supportedBrowsersRegExp.ts
│ └── useMemo.ts
│ ├── tsconfig.json
│ ├── webpack.browserCheck.js
│ ├── webpack.common.js
│ ├── webpack.dev.js
│ └── webpack.prod.js
└── yarn.lock
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [hata6502]
2 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: 'CodeQL'
2 | on:
3 | push:
4 | branches: [master]
5 | pull_request:
6 | branches: [master]
7 | schedule:
8 | - cron: '0 3 * * 1'
9 | jobs:
10 | analyze:
11 | name: Analyze
12 | runs-on: ubuntu-20.04
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | language: ['javascript']
17 | steps:
18 | - name: Checkout repository
19 | uses: actions/checkout@v2
20 | with:
21 | fetch-depth: 2
22 | - run: git checkout HEAD^2
23 | if: ${{ github.event_name == 'pull_request' }}
24 | - name: Initialize CodeQL
25 | uses: github/codeql-action/init@v1
26 | with:
27 | languages: ${{ matrix.language }}
28 | - name: Autobuild
29 | uses: github/codeql-action/autobuild@v1
30 | - name: Perform CodeQL Analysis
31 | uses: github/codeql-action/analyze@v1
32 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy
2 | on:
3 | push:
4 | branches: [master]
5 | jobs:
6 | deploy:
7 | runs-on: ubuntu-20.04
8 | steps:
9 | - uses: actions/checkout@v2
10 | - uses: actions/setup-node@v1
11 | with:
12 | node-version: 14.x
13 | - run: yarn
14 | - run: yarn build
15 | working-directory: packages/website
16 | - run: gzip -r packages/website/docs
17 | - run: for file in $(find packages/website/docs -type f); do mv "${file}" "${file%.gz}"; done
18 | - uses: jakejarvis/s3-sync-action@master
19 | with:
20 | args: --acl public-read --cache-control max-age=86400 --content-encoding gzip --delete --exclude "lp/*" --follow-symlinks
21 | env:
22 | AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
23 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
24 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
25 | SOURCE_DIR: packages/website/docs
26 | - uses: chetan/invalidate-cloudfront-action@master
27 | env:
28 | DISTRIBUTION: ${{ secrets.DISTRIBUTION }}
29 | PATHS: '/*'
30 | AWS_REGION: 'us-east-1'
31 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
32 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
33 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 | on:
3 | push:
4 | branches: [master]
5 | pull_request:
6 | branches: [master]
7 | jobs:
8 | test:
9 | runs-on: ubuntu-20.04
10 | steps:
11 | - uses: actions/checkout@v2
12 | - uses: actions/setup-node@v1
13 | with:
14 | node-version: 14.x
15 | - run: yarn
16 | - run: yarn test
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /packages/core/node_modules
3 | /packages/scrapbox-lint/dist
4 | /packages/scrapbox-lint/node_modules
5 | /packages/website/docs
6 | /packages/website/node_modules
7 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | packages/website/docs/dict/sudachi-synonyms-dictionary.json
2 | packages/website/resources/dict/sudachi-synonyms-dictionary.json
3 | packages/website/src/text-quote-injection.js
4 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 100,
3 | singleQuote: true,
4 | semi: true
5 | };
6 |
--------------------------------------------------------------------------------
/.yarnclean:
--------------------------------------------------------------------------------
1 | @types/react-native
2 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | - Demonstrating empathy and kindness toward other people
21 | - Being respectful of differing opinions, viewpoints, and experiences
22 | - Giving and gracefully accepting constructive feedback
23 | - Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | - Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | - The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | - Trolling, insulting or derogatory comments, and personal or political attacks
33 | - Public or private harassment
34 | - Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | - Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | hato6502@gmail.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Tomoyuki Hata
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 |
Welcome to 校正さん 👋
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | > 文章校正できるメモ帳アプリ
12 |
13 | ### 🏠 [Homepage](https://kohsei-san.hata6502.com/lp/)
14 |
15 | ## Install
16 |
17 | ```sh
18 | yarn
19 | ```
20 |
21 | ## Build
22 |
23 | ```sh
24 | cd packages/website
25 | yarn build
26 | ```
27 |
28 | ## Start server
29 |
30 | ```sh
31 | cd packages/website
32 | yarn start
33 | ```
34 |
35 | ## Develop
36 |
37 | ```sh
38 | cd packages/website
39 | yarn dev
40 | ```
41 |
42 | ## Lint and format
43 |
44 | ```sh
45 | yarn fix
46 | ```
47 |
48 | ## Run tests
49 |
50 | ```sh
51 | yarn test
52 | ```
53 |
54 | ## Author
55 |
56 | **Tomoyuki Hata **
57 |
58 | ## 🤝 Contributing
59 |
60 | Contributions, issues and feature requests are welcome! Feel free to check [issues page](https://github.com/hata6502/kohsei-san/issues).
61 |
62 | ## Show your support
63 |
64 | Give a ⭐️ if this project helped you!
65 |
66 | ---
67 |
68 | _This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_
69 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kohsei-san",
3 | "version": "0.0.0",
4 | "private": true,
5 | "homepage": "https://kohsei-san.hata6502.com/",
6 | "bugs": {
7 | "url": "https://github.com/hata6502/kohsei-san/issues"
8 | },
9 | "repository": "https://github.com/hata6502/kohsei-san",
10 | "funding": {
11 | "type": "github",
12 | "url": "https://github.com/sponsors/hata6502"
13 | },
14 | "license": "MIT",
15 | "author": "Tomoyuki Hata ",
16 | "workspaces": [
17 | "packages/*"
18 | ],
19 | "scripts": {
20 | "_prettier": "prettier '**/*.{html,json,md,yml}'",
21 | "_test-scrapbox-lint": "cd packages/scrapbox-lint && yarn test",
22 | "_test-website": "cd packages/website && yarn test",
23 | "fix": "yarn _prettier --write",
24 | "test": "yarn _prettier --check && yarn _test-scrapbox-lint && yarn _test-website"
25 | },
26 | "resolutions": {
27 | "kuromoji": "hata6502/kuromoji.js",
28 | "kuromojin": "hata6502/kuromojin",
29 | "lodash": "^4.17.20",
30 | "minimist": "^1.2.2",
31 | "node-forge": "^0.10.0",
32 | "prh": "hata6502/prh",
33 | "sudachi-synonyms-dictionary": "hata6502/sudachi-synonyms-dictionary",
34 | "textlint-rule-no-nfd": "hata6502/textlint-rule-no-nfd",
35 | "textlint-rule-prefer-tari-tari": "hata6502/textlint-rule-prefer-tari-tari"
36 | },
37 | "devDependencies": {
38 | "kuromoji": "hata6502/kuromoji.js",
39 | "kuromojin": "hata6502/kuromojin",
40 | "prettier": "^2.0.2",
41 | "sudachi-synonyms-dictionary": "hata6502/sudachi-synonyms-dictionary",
42 | "typescript": "^4.1.3"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "core",
3 | "version": "0.0.0",
4 | "private": true,
5 | "main": "src/index.ts",
6 | "dependencies": {
7 | "@textlint-ja/textlint-rule-no-dropping-i": "hata6502/textlint-rule-no-dropping-i",
8 | "@textlint-ja/textlint-rule-no-filler": "^1.1.0",
9 | "@textlint-ja/textlint-rule-no-insert-dropping-sa": "hata6502/textlint-rule-no-insert-dropping-sa",
10 | "@textlint-ja/textlint-rule-no-insert-re": "^1.0.0",
11 | "@textlint/kernel": "^3.2.1",
12 | "@textlint/textlint-plugin-text": "^4.1.13",
13 | "textlint-filter-rule-urls": "^1.0.1",
14 | "textlint-rule-general-novel-style-ja": "^1.3.0",
15 | "textlint-rule-ja-hiragana-daimeishi": "hata6502/textlint-rule-ja-hiragana-daimeishi",
16 | "textlint-rule-ja-hiragana-fukushi": "hata6502/textlint-rule-ja-hiragana-fukushi",
17 | "textlint-rule-ja-hiragana-hojodoushi": "hata6502/textlint-rule-ja-hiragana-hojodoushi",
18 | "textlint-rule-ja-hiragana-keishikimeishi": "^1.1.0",
19 | "textlint-rule-ja-joyo-or-jinmeiyo-kanji": "^1.0.0",
20 | "textlint-rule-ja-kyoiku-kanji": "^1.0.1",
21 | "textlint-rule-ja-no-mixed-period": "^2.1.1",
22 | "textlint-rule-ja-no-redundant-expression": "^3.0.1",
23 | "textlint-rule-ja-no-successive-word": "^1.2.0",
24 | "textlint-rule-ja-no-weak-phrase": "^2.0.0",
25 | "textlint-rule-ja-simple-user-dictionary": "^1.0.0",
26 | "textlint-rule-ja-unnatural-alphabet": "^2.0.1",
27 | "textlint-rule-max-appearence-count-of-words": "hata6502/textlint-rule-max-appearence-count-of-words",
28 | "textlint-rule-no-difficult-words": "^1.0.0",
29 | "textlint-rule-no-doubled-conjunctive-particle-ga": "^2.0.4",
30 | "textlint-rule-no-hankaku-kana": "^1.0.2",
31 | "textlint-rule-no-kangxi-radicals": "^0.1.0",
32 | "textlint-rule-no-mixed-zenkaku-and-hankaku-alphabet": "^1.0.1",
33 | "textlint-rule-no-zero-width-spaces": "^1.0.0",
34 | "textlint-rule-prefer-tari-tari": "^1.0.3",
35 | "textlint-rule-preset-ja-spacing": "^2.0.2",
36 | "textlint-rule-preset-ja-technical-writing": "hata6502/textlint-rule-preset-ja-technical-writing",
37 | "textlint-rule-preset-japanese": "hata6502/textlint-rule-preset-japanese",
38 | "textlint-rule-preset-jtf-style": "hata6502/textlint-rule-preset-jtf-style",
39 | "textlint-rule-sentence-length": "^3.0.0"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/core/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './lint';
2 |
--------------------------------------------------------------------------------
/packages/core/src/lint.ts:
--------------------------------------------------------------------------------
1 | import { TextlintKernel, TextlintResult } from '@textlint/kernel';
2 | import textlintPluginText from '@textlint/textlint-plugin-text';
3 | // @ts-expect-error 型が定義されていない。
4 | import textlintRuleNoDroppingI from '@textlint-ja/textlint-rule-no-dropping-i';
5 | import textlintRuleNoFiller from '@textlint-ja/textlint-rule-no-filler';
6 | // @ts-expect-error 型が定義されていない。
7 | import textlintRuleNoInsertDroppingSa from '@textlint-ja/textlint-rule-no-insert-dropping-sa';
8 | // @ts-expect-error 型が定義されていない。
9 | import textlintRuleNoInsertRe from '@textlint-ja/textlint-rule-no-insert-re';
10 | // @ts-expect-error 型が定義されていない。
11 | import textlintRuleNoZeroWidthSpaces from 'textlint-rule-no-zero-width-spaces';
12 | import textlintFilterRuleURLs from 'textlint-filter-rule-urls';
13 | // @ts-expect-error 型が定義されていない。
14 | import textlintRuleGeneralNovelStyleJa from 'textlint-rule-general-novel-style-ja';
15 | // @ts-expect-error 型が定義されていない。
16 | import textlintRuleJaHiraganaDaimeishi from 'textlint-rule-ja-hiragana-daimeishi';
17 | // @ts-expect-error 型が定義されていない。
18 | import textlintRuleJaHiraganaFukushi from 'textlint-rule-ja-hiragana-fukushi';
19 | // @ts-expect-error 型が定義されていない。
20 | import textlintRuleJaHiraganaHojodoushi from 'textlint-rule-ja-hiragana-hojodoushi';
21 | // @ts-expect-error 型が定義されていない。
22 | import textlintRuleJaHiraganaKeishikimeishi from 'textlint-rule-ja-hiragana-keishikimeishi';
23 | // @ts-expect-error 型が定義されていない。
24 | import textlintRuleJaJoyoOrJinmeiyoKanji from 'textlint-rule-ja-joyo-or-jinmeiyo-kanji';
25 | // @ts-expect-error 型が定義されていない。
26 | import textlintRuleJaKyoikuKanji from 'textlint-rule-ja-kyoiku-kanji';
27 | // @ts-expect-error 型が定義されていない。
28 | import textlintRuleJaNoMixedPeriod from 'textlint-rule-ja-no-mixed-period';
29 | // @ts-expect-error 型が定義されていない。
30 | import textlintRuleJaNoRedundantExpression from 'textlint-rule-ja-no-redundant-expression';
31 | // @ts-expect-error 型が定義されていない。
32 | import textlintRuleJaNoSuccessiveWord from 'textlint-rule-ja-no-successive-word';
33 | // @ts-expect-error 型が定義されていない。
34 | import textlintRuleJaNoWeakPhrase from 'textlint-rule-ja-no-weak-phrase';
35 | // @ts-expect-error 型が定義されていない。
36 | import textlintRuleJaSimpleUserDictionary from 'textlint-rule-ja-simple-user-dictionary';
37 | // @ts-expect-error 型が定義されていない。
38 | import textlintRuleJaUnnaturalAlphabet from 'textlint-rule-ja-unnatural-alphabet';
39 | // @ts-expect-error 型が定義されていない。
40 | import textlintRuleMaxAppearenceCountOfWords from 'textlint-rule-max-appearence-count-of-words';
41 | // @ts-expect-error 型が定義されていない。
42 | import textlintRuleNoDifficultWords from 'textlint-rule-no-difficult-words';
43 | // @ts-expect-error 型が定義されていない。
44 | import textlintRuleNoDoubledConjunctiveParticleGa from 'textlint-rule-no-doubled-conjunctive-particle-ga';
45 | // @ts-expect-error 型が定義されていない。
46 | import textlintRuleNoHankakuKana from 'textlint-rule-no-hankaku-kana';
47 | // @ts-expect-error 型が定義されていない。
48 | import textlintRuleNoKangxiRadicals from 'textlint-rule-no-kangxi-radicals';
49 | // @ts-expect-error 型が定義されていない。
50 | import textlintRuleNoMixedZenkakuAndHankakuAlphabet from 'textlint-rule-no-mixed-zenkaku-and-hankaku-alphabet';
51 | // @ts-expect-error 型が定義されていない。
52 | import textlintRulePreferTariTari from 'textlint-rule-prefer-tari-tari';
53 | // @ts-expect-error 型が定義されていない。
54 | import textlintRulePresetJaSpacing from 'textlint-rule-preset-ja-spacing';
55 | // @ts-expect-error 型が定義されていない。
56 | import textlintRulePresetJaTechnicalWriting from 'textlint-rule-preset-ja-technical-writing';
57 | // @ts-expect-error 型が定義されていない。
58 | import textlintRulePresetJapanese from 'textlint-rule-preset-japanese';
59 | // @ts-expect-error 型が定義されていない。
60 | import textlintRulePresetJTFStyle from 'textlint-rule-preset-jtf-style';
61 | import textlintRuleSentenceLength from 'textlint-rule-sentence-length';
62 |
63 | // https://scrapbox.io/hata6502/lintOptions
64 | interface LintOption {
65 | presetJaSpacing?: boolean | Record;
66 | presetJaTechnicalWriting?: boolean | Record;
67 | presetJTFStyle?: boolean | Record;
68 | generalNovelStyleJa?: boolean | Record;
69 | jaKyoikuKanji?: boolean | Record;
70 | jaNoMixedPeriod?: boolean | Record;
71 | jaNoWeakPhrase?: boolean | Record;
72 | jaSimpleUserDictionary?: Record;
73 | maxAppearenceCountOfWords?: boolean | Record;
74 | noFiller?: boolean | Record;
75 | }
76 |
77 | const kernel = new TextlintKernel();
78 |
79 | const lint = ({
80 | lintOption,
81 | text,
82 | }: {
83 | lintOption: LintOption;
84 | text: string;
85 | }): Promise =>
86 | kernel.lintText(text, {
87 | ext: '.txt',
88 | filterRules: [
89 | {
90 | ruleId: 'urls',
91 | rule: textlintFilterRuleURLs,
92 | },
93 | ],
94 | plugins: [
95 | {
96 | pluginId: 'text',
97 | plugin: textlintPluginText,
98 | },
99 | ],
100 | rules: [
101 | ...(lintOption.presetJaSpacing
102 | ? Object.keys(textlintRulePresetJaSpacing.rules).map((key) => ({
103 | ruleId: key,
104 | rule: textlintRulePresetJaSpacing.rules[key],
105 | options:
106 | typeof lintOption.presetJaSpacing === 'object'
107 | ? lintOption.presetJaSpacing[key]
108 | : textlintRulePresetJaSpacing.rulesConfig[key],
109 | }))
110 | : []),
111 | ...Object.keys(textlintRulePresetJapanese.rules)
112 | .filter((key) => !['sentence-length'].includes(key))
113 | .map((key) => ({
114 | ruleId: key,
115 | rule: textlintRulePresetJapanese.rules[key],
116 | options: textlintRulePresetJapanese.rulesConfig[key],
117 | })),
118 | ...(lintOption.presetJaTechnicalWriting
119 | ? Object.keys(textlintRulePresetJaTechnicalWriting.rules)
120 | .filter((key) => !['sentence-length'].includes(key))
121 | .map((key) => ({
122 | ruleId: key,
123 | rule: textlintRulePresetJaTechnicalWriting.rules[key],
124 | options:
125 | typeof lintOption.presetJaTechnicalWriting === 'object'
126 | ? lintOption.presetJaTechnicalWriting[key]
127 | : textlintRulePresetJaTechnicalWriting.rulesConfig[key],
128 | }))
129 | : []),
130 | ...(lintOption.presetJTFStyle
131 | ? Object.keys(textlintRulePresetJTFStyle.rules).map((key) => ({
132 | ruleId: key,
133 | rule: textlintRulePresetJTFStyle.rules[key],
134 | options:
135 | typeof lintOption.presetJTFStyle === 'object'
136 | ? lintOption.presetJTFStyle[key]
137 | : textlintRulePresetJTFStyle.rulesConfig[key],
138 | }))
139 | : []),
140 | ...(lintOption.generalNovelStyleJa
141 | ? [
142 | {
143 | ruleId: 'general-novel-style-ja',
144 | rule: textlintRuleGeneralNovelStyleJa,
145 | options:
146 | typeof lintOption.generalNovelStyleJa === 'object' &&
147 | lintOption.generalNovelStyleJa !== null
148 | ? lintOption.generalNovelStyleJa
149 | : undefined,
150 | },
151 | ]
152 | : []),
153 | {
154 | ruleId: 'ja-hiragana-daimeishi',
155 | rule: textlintRuleJaHiraganaDaimeishi,
156 | },
157 | {
158 | ruleId: 'ja-hiragana-fukushi',
159 | rule: textlintRuleJaHiraganaFukushi,
160 | },
161 | {
162 | ruleId: 'ja-hiragana-hojodoushi',
163 | rule: textlintRuleJaHiraganaHojodoushi,
164 | },
165 | {
166 | ruleId: 'ja-hiragana-keishikimeishi',
167 | rule: textlintRuleJaHiraganaKeishikimeishi,
168 | },
169 | {
170 | ruleId: 'ja-joyo-or-jinmeiyo-kanji',
171 | rule: textlintRuleJaJoyoOrJinmeiyoKanji,
172 | },
173 | ...(lintOption.jaKyoikuKanji
174 | ? [
175 | {
176 | ruleId: 'ja-kyoiku-kanji',
177 | rule: textlintRuleJaKyoikuKanji,
178 | options:
179 | typeof lintOption.jaKyoikuKanji === 'object' && lintOption.jaKyoikuKanji !== null
180 | ? lintOption.jaKyoikuKanji
181 | : undefined,
182 | },
183 | ]
184 | : []),
185 | ...(lintOption.jaNoMixedPeriod
186 | ? [
187 | {
188 | ruleId: 'ja-no-mixed-period',
189 | rule: textlintRuleJaNoMixedPeriod,
190 | options:
191 | typeof lintOption.jaNoMixedPeriod === 'object' &&
192 | lintOption.jaNoMixedPeriod !== null
193 | ? lintOption.jaNoMixedPeriod
194 | : undefined,
195 | },
196 | ]
197 | : []),
198 | {
199 | ruleId: 'ja-no-redundant-expression',
200 | rule: textlintRuleJaNoRedundantExpression,
201 | },
202 | {
203 | ruleId: 'ja-no-successive-word',
204 | rule: textlintRuleJaNoSuccessiveWord,
205 | options: {
206 | allow: ['/[\\u2000-\\u2DFF\\u2E00-\\u33FF\\uF900-\\uFFFD]/'],
207 | },
208 | },
209 | ...(lintOption.jaNoWeakPhrase
210 | ? [
211 | {
212 | ruleId: 'ja-no-weak-phrase',
213 | rule: textlintRuleJaNoWeakPhrase,
214 | options:
215 | typeof lintOption.jaNoWeakPhrase === 'object' && lintOption.jaNoWeakPhrase !== null
216 | ? lintOption.jaNoWeakPhrase
217 | : undefined,
218 | },
219 | ]
220 | : []),
221 | {
222 | ruleId: 'ja-simple-user-dictionary',
223 | rule: textlintRuleJaSimpleUserDictionary,
224 | options:
225 | typeof lintOption.jaSimpleUserDictionary === 'object' &&
226 | lintOption.jaSimpleUserDictionary !== null
227 | ? lintOption.jaSimpleUserDictionary
228 | : undefined,
229 | },
230 | {
231 | ruleId: 'ja-unnatural-alphabet',
232 | rule: textlintRuleJaUnnaturalAlphabet,
233 | },
234 | ...(lintOption.maxAppearenceCountOfWords
235 | ? [
236 | {
237 | ruleId: 'max-appearence-count-of-words',
238 | rule: textlintRuleMaxAppearenceCountOfWords,
239 | options:
240 | typeof lintOption.maxAppearenceCountOfWords === 'object' &&
241 | lintOption.maxAppearenceCountOfWords !== null
242 | ? lintOption.maxAppearenceCountOfWords
243 | : undefined,
244 | },
245 | ]
246 | : []),
247 | {
248 | ruleId: 'no-difficult-words',
249 | rule: textlintRuleNoDifficultWords,
250 | },
251 | {
252 | ruleId: 'no-doubled-conjunctive-particle-ga',
253 | rule: textlintRuleNoDoubledConjunctiveParticleGa,
254 | },
255 | {
256 | ruleId: 'no-dropping-i',
257 | rule: textlintRuleNoDroppingI,
258 | },
259 | ...(lintOption.noFiller
260 | ? [
261 | {
262 | ruleId: 'no-filler',
263 | rule: textlintRuleNoFiller,
264 | options:
265 | typeof lintOption.noFiller === 'object' && lintOption.noFiller !== null
266 | ? lintOption.noFiller
267 | : undefined,
268 | },
269 | ]
270 | : []),
271 | {
272 | ruleId: 'no-hankaku-kana',
273 | rule: textlintRuleNoHankakuKana,
274 | },
275 | {
276 | ruleId: 'no-insert-dropping-sa',
277 | rule: textlintRuleNoInsertDroppingSa,
278 | },
279 | {
280 | ruleId: 'no-insert-re',
281 | rule: textlintRuleNoInsertRe,
282 | },
283 | {
284 | ruleId: 'no-kangxi-radicals',
285 | rule: textlintRuleNoKangxiRadicals,
286 | },
287 | {
288 | ruleId: 'no-mixed-zenkaku-and-hankaku-alphabet',
289 | rule: textlintRuleNoMixedZenkakuAndHankakuAlphabet,
290 | },
291 | {
292 | ruleId: 'no-zero-width-spaces',
293 | rule: textlintRuleNoZeroWidthSpaces,
294 | },
295 | {
296 | ruleId: 'prefer-tari-tari',
297 | rule: textlintRulePreferTariTari,
298 | },
299 | {
300 | ruleId: 'sentence-length',
301 | rule: textlintRuleSentenceLength,
302 | },
303 | ],
304 | });
305 |
306 | export { lint };
307 | export type { LintOption };
308 |
--------------------------------------------------------------------------------
/packages/scrapbox-lint/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "scrapbox-lint",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "webpack",
7 | "test": "tsc --noEmit"
8 | },
9 | "devDependencies": {
10 | "@babel/core": "^7.16.0",
11 | "babel-loader": "^8.2.3",
12 | "babel-plugin-static-fs": "^3.0.0",
13 | "core": "0.0.0",
14 | "os-browserify": "^0.3.0",
15 | "ts-loader": "^9.2.3",
16 | "typescript": "^4.8.4",
17 | "webpack": "^5.40.0",
18 | "webpack-cli": "^4.7.2"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/scrapbox-lint/src/index.ts:
--------------------------------------------------------------------------------
1 | import { LintOption, lint } from 'core';
2 |
3 | export const runScrapboxLint = async ({ lintOptionURL }: { lintOptionURL?: string }) => {
4 | const popoverElement = document.createElement('div');
5 |
6 | popoverElement.id = 'scrapbox-lint-popover';
7 | popoverElement.style.display = 'none';
8 | popoverElement.style.position = 'absolute';
9 | popoverElement.style.backgroundColor = '#ffffff';
10 | popoverElement.style.borderRadius = '4px';
11 | popoverElement.style.boxShadow =
12 | 'rgb(0 0 0 / 20%) 0px 5px 5px -3px, rgb(0 0 0 / 14%) 0px 8px 10px 1px, rgb(0 0 0 / 12%) 0px 3px 14px 2px';
13 | popoverElement.style.marginBottom = '16px';
14 | popoverElement.style.marginRight = '16px';
15 | popoverElement.style.maxWidth = '600px';
16 | popoverElement.style.padding = '16px';
17 |
18 | document.body.insertBefore(popoverElement, document.querySelector('#drawer-container'));
19 |
20 | const pinElements: HTMLDivElement[] = [];
21 | let abortUpdatingPinsController: AbortController;
22 | let result: Awaited> | undefined;
23 | let updatingPinsPromise: Promise;
24 |
25 | const updatePins = () => {
26 | abortUpdatingPinsController?.abort();
27 | abortUpdatingPinsController = new AbortController();
28 |
29 | return (updatingPinsPromise = (async ({ signal }) => {
30 | try {
31 | await updatingPinsPromise;
32 | } catch (exception) {
33 | if (exception instanceof Error && exception.name !== 'AbortError') {
34 | throw exception;
35 | }
36 | } finally {
37 | popoverElement.style.display = 'none';
38 |
39 | const messages = result?.messages ?? [];
40 | let messageIndex;
41 |
42 | for (messageIndex = 0; messageIndex < messages.length; messageIndex++) {
43 | const message = messages[messageIndex];
44 |
45 | if (signal.aborted) {
46 | throw new DOMException('Aborted', 'AbortError');
47 | }
48 |
49 | const bodyRect = document.body.getBoundingClientRect();
50 | // @ts-expect-error
51 | const lineID = scrapbox.Page.lines[message.line - 1].id;
52 |
53 | const lineElement = document.querySelector(`#L${lineID}`);
54 | const charElement = lineElement?.querySelector(`.c-${message.column - 1}`);
55 |
56 | if (!lineElement || !charElement) {
57 | // Abort without exception when Scrapbox layout was changed.
58 | return;
59 | }
60 |
61 | const lineRect = lineElement.getBoundingClientRect();
62 | const charRect = charElement.getBoundingClientRect();
63 |
64 | const pinElement = document.createElement('div');
65 | const pinTop = charRect.top - bodyRect.top;
66 | const pinHeight = charRect.height;
67 |
68 | pinElement.style.position = 'absolute';
69 | pinElement.style.left = `${charRect.left - bodyRect.left}px`;
70 | pinElement.style.top = `${pinTop}px`;
71 | pinElement.style.height = `${pinHeight}px`;
72 | pinElement.style.width = `${charRect.width}px`;
73 | pinElement.style.backgroundColor = 'rgba(241, 93, 105, 0.5)';
74 |
75 | pinElement.addEventListener('click', (event) => {
76 | event.stopPropagation();
77 |
78 | popoverElement.textContent = message.message;
79 |
80 | popoverElement.style.display = 'block';
81 | popoverElement.style.left = `${lineRect.left - bodyRect.left}px`;
82 | popoverElement.style.top = `${pinTop + pinHeight}px`;
83 | });
84 |
85 | document.body.insertBefore(
86 | pinElement,
87 | document.querySelector('#scrapbox-lint-popover') ??
88 | document.querySelector('#drawer-container')
89 | );
90 |
91 | pinElements[messageIndex]?.remove();
92 | pinElements[messageIndex] = pinElement;
93 |
94 | await new Promise((resolve) => setTimeout(resolve));
95 | }
96 |
97 | pinElements.splice(messageIndex).forEach((pinElement) => pinElement.remove());
98 | }
99 | })({ signal: abortUpdatingPinsController.signal }));
100 | };
101 |
102 | // @ts-expect-error
103 | const getText = () => scrapbox.Page.lines.map((line) => line.text).join('\n');
104 |
105 | const worker = new Worker('/api/code/hata6502/scrapbox-lint-worker/index.js');
106 |
107 | worker.addEventListener('message', (event) => {
108 | const data = event.data;
109 |
110 | switch (data.type) {
111 | case 'result': {
112 | // @ts-expect-error
113 | if (scrapbox.Layout !== 'page' || getText() !== data.text) {
114 | return;
115 | }
116 |
117 | result = data.result;
118 | updatePins();
119 |
120 | break;
121 | }
122 | // TODO: exhaustive check
123 | }
124 | });
125 |
126 | const pageElement = document.querySelector('.page');
127 |
128 | if (!pageElement) {
129 | throw new Error('Page element not found.');
130 | }
131 |
132 | new MutationObserver(updatePins).observe(pageElement, {
133 | attributes: true,
134 | characterData: true,
135 | childList: true,
136 | subtree: true,
137 | });
138 |
139 | new ResizeObserver(updatePins).observe(document.body);
140 |
141 | const getLintOption = async (): Promise => {
142 | if (!lintOptionURL) {
143 | return {};
144 | }
145 |
146 | const response = await fetch(lintOptionURL);
147 |
148 | if (!response.ok) {
149 | throw new Error(`${response.status} ${response.statusText}`);
150 | }
151 |
152 | const { jaSimpleUserDictionary, ...otherLintOption } = await response.json();
153 | const dictionary = [...(jaSimpleUserDictionary?.dictionary ?? [])];
154 |
155 | for (const dictionaryPageName of jaSimpleUserDictionary?.dictionaryPages ?? []) {
156 | const response = await fetch(`/api/pages/${dictionaryPageName}`);
157 |
158 | if (!response.ok) {
159 | throw new Error(`${response.status} ${response.statusText}`);
160 | }
161 |
162 | const dictionaryPage = await response.json();
163 | const lines = dictionaryPage.lines.map((line: any) => line.text);
164 |
165 | while (lines.length) {
166 | const patternMatch = lines[0]?.match(/^\s(.*)$/);
167 |
168 | if (patternMatch) {
169 | lines.shift();
170 |
171 | const messageMatch = lines[0]?.match(/^\s\s(.*)$/);
172 | let message;
173 |
174 | if (messageMatch) {
175 | lines.shift();
176 |
177 | message = messageMatch[1].trim();
178 | }
179 |
180 | dictionary.push({ pattern: patternMatch[1].trim(), message });
181 | } else {
182 | lines.shift();
183 | }
184 | }
185 | }
186 |
187 | return {
188 | ...otherLintOption,
189 | jaSimpleUserDictionary: {
190 | ...jaSimpleUserDictionary,
191 | dictionary,
192 | },
193 | };
194 | };
195 |
196 | const lintOption = await getLintOption();
197 |
198 | const lintPage = () => {
199 | // @ts-expect-error
200 | if (scrapbox.Layout !== 'page') {
201 | result = undefined;
202 | updatePins();
203 |
204 | return;
205 | }
206 |
207 | worker.postMessage({ type: 'lint', lintOption, text: getText() });
208 | };
209 |
210 | lintPage();
211 | // @ts-expect-error
212 | scrapbox.on('lines:changed', lintPage);
213 | // @ts-expect-error
214 | scrapbox.on('layout:changed', lintPage);
215 | // @ts-expect-error
216 | scrapbox.on('project:changed', () => location.reload());
217 | };
218 |
--------------------------------------------------------------------------------
/packages/scrapbox-lint/src/worker.ts:
--------------------------------------------------------------------------------
1 | import { lint } from 'core';
2 |
3 | // @ts-expect-error
4 | self.kuromojin = {
5 | dicPath: 'https://storage.googleapis.com/scrapbox-lint/',
6 | };
7 |
8 | let abortHandlingMessageController: AbortController;
9 |
10 | self.addEventListener('message', (event) => {
11 | abortHandlingMessageController?.abort();
12 | abortHandlingMessageController = new AbortController();
13 |
14 | return (async ({ signal }) => {
15 | const data = event.data;
16 |
17 | switch (data.type) {
18 | case 'lint': {
19 | const { lintOption, text } = data;
20 | const result = await lint({ lintOption, text });
21 |
22 | if (signal.aborted) {
23 | throw new DOMException('Aborted', 'AbortError');
24 | }
25 |
26 | postMessage({
27 | type: 'result',
28 | result,
29 | text,
30 | });
31 |
32 | break;
33 | }
34 |
35 | // TODO: exhaustive check
36 | }
37 | })({ signal: abortHandlingMessageController.signal });
38 | });
39 |
40 | lint({ lintOption: {}, text: '初回校正時でもキャッシュにヒットさせるため。' });
41 |
--------------------------------------------------------------------------------
/packages/scrapbox-lint/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "esModuleInterop": true,
5 | "jsx": "react-jsx",
6 | "lib": ["DOM", "DOM.Iterable", "ES2020"],
7 | "module": "ES2020",
8 | "moduleResolution": "node",
9 | "noPropertyAccessFromIndexSignature": true,
10 | "outDir": "dist",
11 | "pretty": true,
12 | "skipLibCheck": true,
13 | "strict": true,
14 | "target": "ES2020"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/scrapbox-lint/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | const staticFs = require('babel-plugin-static-fs');
4 | const webpack = require('webpack');
5 |
6 | const commonConfig = {
7 | mode: 'production',
8 | experiments: {
9 | outputModule: true,
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.[jt]sx?$/,
15 | use: [
16 | {
17 | loader: 'babel-loader',
18 | options: {
19 | plugins: [
20 | [
21 | staticFs,
22 | {
23 | target: 'browser',
24 | dynamic: false,
25 | },
26 | ],
27 | ],
28 | },
29 | },
30 | {
31 | loader: 'ts-loader',
32 | options: {
33 | transpileOnly: true,
34 | },
35 | },
36 | ],
37 | },
38 | ],
39 | },
40 | output: {
41 | // Prevent to use mjs extension.
42 | filename: '[name].js',
43 | },
44 | plugins: [
45 | new webpack.ProvidePlugin({
46 | process: 'process/browser',
47 | }),
48 | ],
49 | resolve: {
50 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
51 | fallback: { os: require.resolve('os-browserify/browser') },
52 | },
53 | };
54 |
55 | module.exports = [
56 | {
57 | ...commonConfig,
58 | entry: {
59 | 'scrapbox-lint-main': './src/index.ts',
60 | },
61 | output: {
62 | ...commonConfig.output,
63 | library: {
64 | type: 'module',
65 | },
66 | },
67 | },
68 | {
69 | ...commonConfig,
70 | entry: {
71 | 'scrapbox-lint-worker': './src/worker.ts',
72 | },
73 | },
74 | ];
75 |
--------------------------------------------------------------------------------
/packages/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "website",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "_generate-supported-browsers": "browserslist --json > src/supportedBrowsers.json",
7 | "_generate-supported-browsers-regexp": "echo \"const supportedBrowsersRegExp = $(browserslist-useragent-regexp --allowHigherVersions);\n\nexport { supportedBrowsersRegExp };\" > src/supportedBrowsersRegExp.ts",
8 | "build": "rimraf docs && webpack --config webpack.prod.js",
9 | "build-browser-check": "yarn _generate-supported-browsers && yarn _generate-supported-browsers-regexp && webpack --config webpack.browserCheck.js",
10 | "dev": "webpack-dev-server --config webpack.dev.js",
11 | "start": "http-server docs",
12 | "test": "tsc --noEmit"
13 | },
14 | "browserslist": [
15 | "extends browserslist-config-google/no-ie",
16 | "not firefox >= 81"
17 | ],
18 | "devDependencies": {
19 | "@babel/core": "^7.15.0",
20 | "@babel/preset-env": "^7.8.7",
21 | "@babel/preset-react": "^7.8.3",
22 | "@babel/preset-typescript": "^7.15.0",
23 | "@material-ui/core": "^4.12.2",
24 | "@material-ui/icons": "^4.11.2",
25 | "@material-ui/lab": "^4.0.0-alpha.60",
26 | "@sentry/browser": "^5.13.0",
27 | "@types/react": "^17.0.14",
28 | "@types/react-dom": "^17.0.9",
29 | "@types/react-helmet": "^6.1.2",
30 | "@types/styled-components": "^5.1.21",
31 | "@types/uuid": "^7.0.0",
32 | "babel-loader": "^8.0.6",
33 | "babel-plugin-istanbul": "^6.0.0",
34 | "babel-plugin-static-fs": "^3.0.0",
35 | "browserslist": "^4.16.1",
36 | "browserslist-config-google": "^2.0.0",
37 | "browserslist-useragent-regexp": "^2.1.1",
38 | "copy-webpack-plugin": "^5.1.1",
39 | "core": "0.0.0",
40 | "core-js": "3",
41 | "http-server": "^0.12.1",
42 | "path": "^0.12.7",
43 | "react": "^17.0.2",
44 | "react-dom": "^17.0.2",
45 | "react-helmet": "^6.0.0",
46 | "react-is": "^17.0.2",
47 | "regenerator-runtime": "^0.13.5",
48 | "rimraf": "^3.0.2",
49 | "styled-components": "^5.3.3",
50 | "typescript": "^4.8.4",
51 | "uuid": "^8.3.2",
52 | "wait-on": "^5.3.0",
53 | "webpack": "^4.41.5",
54 | "webpack-cli": "^3.3.10",
55 | "webpack-dev-server": "^3.10.3",
56 | "webpack-merge": "^4.2.2",
57 | "workbox-webpack-plugin": "^5.0.0"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/packages/website/resources/8737dd05a68d04d808dfdb81c6783be1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hata6502/kohsei-san/71654b51dee33ad3a17229f98ea63816f8679394/packages/website/resources/8737dd05a68d04d808dfdb81c6783be1.png
--------------------------------------------------------------------------------
/packages/website/resources/browserCheck.js:
--------------------------------------------------------------------------------
1 | !function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=1)}([function(e){e.exports=JSON.parse('{"browsers":["and_chr 87","chrome 87","chrome 86","chrome 85","edge 87","edge 86","ios_saf 14.0-14.3","ios_saf 13.4-13.7","ios_saf 13.3","ios_saf 13.2","ios_saf 13.0-13.1","safari 14","safari 13.1","safari 13"]}')},function(e,t,r){"use strict";r.r(t);var n=r(0);/((CPU[ +]OS|iPhone[ +]OS|CPU[ +]iPhone|CPU IPhone OS)[ +]+(13[_.]0|13[_.]([1-9]|\d{2,})|13[_.]7|13[_.]([8-9]|\d{2,})|(1[4-9]|[2-9]\d|\d{3,})[_.]\d+|14[_.]0|14[_.]([1-9]|\d{2,})|14[_.]3|14[_.]([4-9]|\d{2,})|(1[5-9]|[2-9]\d|\d{3,})[_.]\d+)(?:[_.]\d+)?)|(Edge\/(86(?:\.0)?|86(?:\.([1-9]|\d{2,}))?|(8[7-9]|9\d|\d{3,})(?:\.\d+)?))|((Chromium|Chrome)\/(85\.0|85\.([1-9]|\d{2,})|(8[6-9]|9\d|\d{3,})\.\d+)(?:\.\d+)?)|(Version\/(13\.0|13\.([1-9]|\d{2,})|(1[4-9]|[2-9]\d|\d{3,})\.\d+|14\.0|14\.([1-9]|\d{2,})|(1[5-9]|[2-9]\d|\d{3,})\.\d+)(?:\.\d+)? Safari\/)/.test(navigator.userAgent)||alert("校正さんを使用するには、下記のウェブブラウザからアクセスしてください。\n\n".concat(n.browsers.map((function(e){return"・".concat(e)})).join("\n"),"\n\nTwitter https://twitter.com/hata6502\nこのアプリについて https://github.com/hata6502/kohsei-san/blob/master/README.md\n"))}]);
--------------------------------------------------------------------------------
/packages/website/resources/dict/base.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hata6502/kohsei-san/71654b51dee33ad3a17229f98ea63816f8679394/packages/website/resources/dict/base.dat
--------------------------------------------------------------------------------
/packages/website/resources/dict/cc.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hata6502/kohsei-san/71654b51dee33ad3a17229f98ea63816f8679394/packages/website/resources/dict/cc.dat
--------------------------------------------------------------------------------
/packages/website/resources/dict/check.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hata6502/kohsei-san/71654b51dee33ad3a17229f98ea63816f8679394/packages/website/resources/dict/check.dat
--------------------------------------------------------------------------------
/packages/website/resources/dict/tid.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hata6502/kohsei-san/71654b51dee33ad3a17229f98ea63816f8679394/packages/website/resources/dict/tid.dat
--------------------------------------------------------------------------------
/packages/website/resources/dict/tid_map.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hata6502/kohsei-san/71654b51dee33ad3a17229f98ea63816f8679394/packages/website/resources/dict/tid_map.dat
--------------------------------------------------------------------------------
/packages/website/resources/dict/unk.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hata6502/kohsei-san/71654b51dee33ad3a17229f98ea63816f8679394/packages/website/resources/dict/unk.dat
--------------------------------------------------------------------------------
/packages/website/resources/dict/unk_char.dat:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
--------------------------------------------------------------------------------
/packages/website/resources/dict/unk_map.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hata6502/kohsei-san/71654b51dee33ad3a17229f98ea63816f8679394/packages/website/resources/dict/unk_map.dat
--------------------------------------------------------------------------------
/packages/website/resources/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hata6502/kohsei-san/71654b51dee33ad3a17229f98ea63816f8679394/packages/website/resources/favicon.png
--------------------------------------------------------------------------------
/packages/website/resources/googleAnalytics.js:
--------------------------------------------------------------------------------
1 | window.dataLayer = window.dataLayer || [];
2 | function gtag() {
3 | dataLayer.push(arguments);
4 | }
5 | gtag('js', new Date());
6 |
7 | gtag('config', 'UA-106651880-5');
8 |
--------------------------------------------------------------------------------
/packages/website/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | 校正さん
57 |
58 |
62 |
63 |
64 |
65 |
66 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | 校正さんを使用するには、JavaScript を有効にしてください。
89 |
90 |
91 | Twitter
92 |
93 | このアプリについて
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/packages/website/resources/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "lang": "ja-JP",
3 | "name": "校正さん",
4 | "short_name": "校正さん",
5 | "start_url": ".",
6 | "display": "standalone",
7 | "icons": [
8 | {
9 | "src": "favicon.png",
10 | "sizes": "512x512",
11 | "type": "image/png"
12 | }
13 | ],
14 | "share_target": {
15 | "action": "./",
16 | "method": "GET",
17 | "enctype": "application/x-www-form-urlencoded",
18 | "params": {
19 | "title": "title",
20 | "text": "text",
21 | "url": "url"
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/website/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useReducer, useState } from 'react';
2 | import { Helmet } from 'react-helmet';
3 | import styled from 'styled-components';
4 | import Alert from '@material-ui/lab/Alert';
5 | import AppBar from '@material-ui/core/AppBar';
6 | import CircularProgress from '@material-ui/core/CircularProgress';
7 | import Drawer from '@material-ui/core/Drawer';
8 | import Hidden from '@material-ui/core/Hidden';
9 | import IconButton from '@material-ui/core/IconButton';
10 | import Snackbar from '@material-ui/core/Snackbar';
11 | import Toolbar from '@material-ui/core/Toolbar';
12 | import Typography from '@material-ui/core/Typography';
13 | import { makeStyles } from '@material-ui/core/styles';
14 | import MenuIcon from '@material-ui/icons/Menu';
15 | import { Edit } from './Edit';
16 | import Empty from './Empty';
17 | import Sidebar from './Sidebar';
18 | import { useMemo } from './useMemo';
19 |
20 | const sidebarWidth = 250;
21 |
22 | const Main = styled.main`
23 | flex-grow: 1;
24 | `;
25 |
26 | const Navigation = styled.nav`
27 | ${({ theme }) => `
28 | ${theme.breakpoints.up('md')} {
29 | flex-shrink: 0;
30 | width: ${sidebarWidth}px;
31 | }
32 | `}
33 | `;
34 |
35 | const Root = styled.div`
36 | display: flex;
37 | `;
38 |
39 | const Title = styled(Typography)`
40 | ${({ theme }) => `
41 | margin-left: ${theme.spacing(1)}px;
42 | `}
43 | `;
44 |
45 | const TopBar = styled(AppBar)`
46 | ${({ theme }) => `
47 | ${theme.breakpoints.up('md')} {
48 | width: calc(100% - ${sidebarWidth}px);
49 | }
50 | `}
51 |
52 | /* Sentry のレポートダイアログを最前面に表示するため */
53 | z-index: 998;
54 | `;
55 |
56 | const useStyles = makeStyles((theme) => ({
57 | toolbar: theme.mixins.toolbar,
58 | }));
59 |
60 | const App: React.FunctionComponent<{ lintWorker: Worker }> = React.memo(({ lintWorker }) => {
61 | const {
62 | dispatchMemoId,
63 | dispatchMemos,
64 | isSaveErrorOpen,
65 | memoId,
66 | memos,
67 | setIsSaveErrorOpen,
68 | titleParam,
69 | // eslint-disable-next-line react-hooks/exhaustive-deps
70 | } = useMemo();
71 |
72 | const [isLinting, dispatchIsLinting] = useReducer((_: boolean, action: boolean) => action, false);
73 |
74 | const [isLintingHeavy, dispatchIsLintingHeavy] = useReducer(
75 | (_: boolean, action: boolean) => action,
76 | false
77 | );
78 |
79 | const [isSidebarOpen, dispatchIsSidebarOpen] = useState(false);
80 | const [isCopiedSnackbarOpen, dispatchIsCopiedSnackbarOpen] = useState(false);
81 |
82 | const classes = useStyles();
83 |
84 | const handleMenuIconClick = useCallback(
85 | () => dispatchIsSidebarOpen(true),
86 | [dispatchIsSidebarOpen]
87 | );
88 | const handleSaveErrorClose = useCallback(() => setIsSaveErrorOpen(false), [setIsSaveErrorOpen]);
89 | const handleSidebarClose = useCallback(
90 | () => dispatchIsSidebarOpen(false),
91 | [dispatchIsSidebarOpen]
92 | );
93 |
94 | const handleCopiedSnackbarClose = useCallback(
95 | () => dispatchIsCopiedSnackbarOpen(false),
96 | [dispatchIsCopiedSnackbarOpen]
97 | );
98 |
99 | const memo = memos.find(({ id }) => id === memoId);
100 | const title = titleParam === null ? '校正さん' : `「${titleParam}」の校正結果 | 校正さん`;
101 |
102 | const sidebarContent = (
103 |
110 | );
111 |
112 | return (
113 |
114 |
115 | {title}
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | {isLinting ? (
126 |
127 | ) : (
128 |
129 | )}
130 |
131 |
132 | {isLinting ? (isLintingHeavy ? 'お待ちください…' : '校正中…') : '校正さん'}
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | {sidebarContent}
141 |
142 |
143 |
144 |
145 |
146 | {sidebarContent}
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 | {memo ? (
155 |
168 | ) : (
169 |
170 | )}
171 |
172 |
173 |
178 | メモをコピーしました。
179 |
180 |
181 |
182 |
183 | メモを保存できませんでした。
184 | メモを他の場所に保存してから、アプリをインストールしてみてください。
185 |
186 |
187 |
188 | );
189 | });
190 |
191 | export default App;
192 |
--------------------------------------------------------------------------------
/packages/website/src/Edit/Edit.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useState } from 'react';
2 | import styled from 'styled-components';
3 | import Box from '@material-ui/core/Box';
4 | import Button from '@material-ui/core/Button';
5 | import Chip from '@material-ui/core/Chip';
6 | import Container from '@material-ui/core/Container';
7 | import Dialog from '@material-ui/core/Dialog';
8 | import DialogActions from '@material-ui/core/DialogActions';
9 | import DialogContent from '@material-ui/core/DialogContent';
10 | import DialogContentText from '@material-ui/core/DialogContentText';
11 | import DialogTitle from '@material-ui/core/DialogTitle';
12 | import Grid from '@material-ui/core/Grid';
13 | import Paper from '@material-ui/core/Paper';
14 | import ShareIcon from '@material-ui/icons/Share';
15 | import { v4 as uuidv4 } from 'uuid';
16 | import type { LintWorkerLintMessage, LintWorkerResultMessage } from '../lintWorker';
17 | import { useDispatchSetting } from '../useMemo';
18 | import type { Memo, MemosAction } from '../useMemo';
19 | import { SettingDialog } from './SettingDialog';
20 | import { TextContainer } from './TextContainer';
21 |
22 | const lintingTimeoutLimitMS = 10000;
23 |
24 | const EditContainer = styled(Container)`
25 | ${({ theme }) => `
26 | margin-bottom: ${theme.spacing(2)}px;
27 | margin-top: ${theme.spacing(2)}px;
28 | `}
29 | `;
30 |
31 | const Edit: React.FunctionComponent<{
32 | dispatchIsCopiedSnackbarOpen: React.Dispatch;
33 | dispatchIsLinting: React.Dispatch;
34 | dispatchIsLintingHeavy: React.Dispatch;
35 | dispatchIsSidebarOpen: React.Dispatch;
36 | dispatchMemoId: React.Dispatch;
37 | dispatchMemos: React.Dispatch;
38 | isLinting: boolean;
39 | lintWorker: Worker;
40 | memo: Memo;
41 | memos: Memo[];
42 | }> = React.memo(
43 | ({
44 | dispatchIsCopiedSnackbarOpen,
45 | dispatchIsLinting,
46 | dispatchIsLintingHeavy,
47 | dispatchIsSidebarOpen,
48 | dispatchMemoId,
49 | dispatchMemos,
50 | isLinting,
51 | lintWorker,
52 | memo,
53 | memos,
54 | }) => {
55 | const [isTextContainerFocused, dispatchIsTextContainerFocused] = useState(false);
56 |
57 | const [isSettingDialogOpen, setIsSettingDialogOpen] = useState(false);
58 | const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
59 |
60 | const dispatchSetting = useDispatchSetting({ dispatchMemos, memoId: memo.id });
61 |
62 | useEffect(
63 | () => () => {
64 | dispatchIsLinting(false);
65 | },
66 | [dispatchIsLinting]
67 | );
68 |
69 | useEffect(() => {
70 | if (!lintWorker || memo.result) {
71 | return;
72 | }
73 |
74 | const userDictionaryMemo = memos.find(
75 | ({ id }) => id === memo.setting.lintOption.userDictionaryMemoId
76 | );
77 | const professionalModeLintOption = {
78 | ...memo.setting.lintOption,
79 | jaSimpleUserDictionary: {
80 | dictionary:
81 | userDictionaryMemo?.text
82 | .trim()
83 | .split('\n')
84 | .slice(1)
85 | .join('\n')
86 | .split('\n\n')
87 | .flatMap((section) => {
88 | const lines = section.trim().split('\n');
89 | return lines[0]
90 | ? [{ pattern: lines[0], message: lines.slice(1).join('\n').trim() || undefined }]
91 | : [];
92 | }) ?? [],
93 | },
94 | };
95 |
96 | const message: LintWorkerLintMessage = {
97 | lintOption: {
98 | professional: professionalModeLintOption,
99 | standard: {},
100 | }[memo.setting.mode],
101 | text: memo.text,
102 | };
103 |
104 | lintWorker.postMessage(message);
105 |
106 | const lintingTimeoutID = setTimeout(
107 | () => dispatchIsLintingHeavy(true),
108 | lintingTimeoutLimitMS
109 | );
110 |
111 | dispatchIsLinting(true);
112 | dispatchIsLintingHeavy(false);
113 |
114 | return () => clearTimeout(lintingTimeoutID);
115 | }, [
116 | dispatchIsLinting,
117 | dispatchIsLintingHeavy,
118 | dispatchMemos,
119 | lintWorker,
120 | memo.id,
121 | memo.result,
122 | memo.setting.lintOption,
123 | memo.setting.mode,
124 | memo.text,
125 | ]);
126 |
127 | useEffect(() => {
128 | const handleLintWorkerError = () => {
129 | dispatchIsLinting(false);
130 |
131 | throw new Error();
132 | };
133 |
134 | const handleLintWorkerMessage = (event: MessageEvent) => {
135 | if (event.data.text !== memo.text) {
136 | return;
137 | }
138 |
139 | dispatchMemos((prevMemos) =>
140 | prevMemos.map((prevMemo) => ({
141 | ...prevMemo,
142 | ...(prevMemo.id === memo.id && { result: event.data.result }),
143 | }))
144 | );
145 | };
146 |
147 | lintWorker.addEventListener('error', handleLintWorkerError);
148 | lintWorker.addEventListener('message', handleLintWorkerMessage);
149 |
150 | return () => {
151 | lintWorker.removeEventListener('error', handleLintWorkerError);
152 | lintWorker.removeEventListener('message', handleLintWorkerMessage);
153 | };
154 | }, [dispatchIsLinting, dispatchMemos, lintWorker, memo.id, memo.text]);
155 |
156 | const shouldDisplayResult = !isTextContainerFocused && !isLinting;
157 |
158 | const handleShareClick = useCallback(async () => {
159 | try {
160 | await navigator.share?.({
161 | text: memo.text,
162 | });
163 | } catch (exception) {
164 | if (!(exception instanceof DOMException) || exception.code !== DOMException.ABORT_ERR) {
165 | throw exception;
166 | }
167 | }
168 | }, [memo.text]);
169 |
170 | const handleSettingButtonClick = useCallback(() => setIsSettingDialogOpen(true), []);
171 | const handleSettingDialogClose = useCallback(() => setIsSettingDialogOpen(false), []);
172 |
173 | const handleCopyButtonClick = useCallback(() => {
174 | const id = uuidv4();
175 |
176 | dispatchMemos((prevMemos) => [
177 | ...prevMemos,
178 | {
179 | ...memo,
180 | id,
181 | },
182 | ]);
183 |
184 | dispatchIsCopiedSnackbarOpen(true);
185 | dispatchIsSidebarOpen(true);
186 | dispatchMemoId(id);
187 | }, [dispatchIsCopiedSnackbarOpen, dispatchIsSidebarOpen, dispatchMemoId, dispatchMemos, memo]);
188 |
189 | const handleDeleteButtonClick = useCallback(() => setIsDeleteDialogOpen(true), []);
190 |
191 | const handleDeleteDialogAgree = useCallback(() => {
192 | dispatchMemos((prevMemos) => prevMemos.filter(({ id }) => id !== memo.id));
193 | }, [dispatchMemos, memo.id]);
194 |
195 | const handleDeleteDialogClose = useCallback(
196 | () => setIsDeleteDialogOpen(false),
197 | [setIsDeleteDialogOpen]
198 | );
199 |
200 | return (
201 |
202 |
203 |
204 |
205 |
206 |
207 |
211 |
212 |
213 |
214 |
222 |
223 |
224 |
225 | {
226 | // @ts-expect-error navigator.share() が存在しない環境もある。
227 | navigator.share && (
228 |
229 | }
234 | >
235 | 共有
236 |
237 |
238 | )
239 | }
240 |
241 |
242 |
243 | 校正設定
244 |
245 |
246 |
247 |
248 |
249 | コピー
250 |
251 |
252 |
253 |
254 |
255 | 削除
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
271 |
272 |
273 | メモを削除しますか?
274 |
275 |
276 | 削除を元に戻すことはできません。
277 |
278 |
279 |
280 |
281 | 削除しない
282 |
283 |
284 |
285 | 削除する
286 |
287 |
288 |
289 |
290 | );
291 | }
292 | );
293 |
294 | export { Edit };
295 |
--------------------------------------------------------------------------------
/packages/website/src/Edit/SettingDialog.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import Box from '@material-ui/core/Box';
3 | import Checkbox from '@material-ui/core/Checkbox';
4 | import Dialog from '@material-ui/core/Dialog';
5 | import type { DialogProps } from '@material-ui/core/Dialog';
6 | import DialogContent from '@material-ui/core/DialogContent';
7 | import DialogTitle from '@material-ui/core/DialogTitle';
8 | import FormControl from '@material-ui/core/FormControl';
9 | import FormControlLabel from '@material-ui/core/FormControlLabel';
10 | import type { FormControlLabelProps } from '@material-ui/core/FormControlLabel';
11 | import InputLabel from '@material-ui/core/InputLabel';
12 | import MenuItem from '@material-ui/core/MenuItem';
13 | import Radio from '@material-ui/core/Radio';
14 | import Select, { SelectProps } from '@material-ui/core/Select';
15 | import Typography from '@material-ui/core/Typography';
16 | import type { DispatchSetting, Memo, Setting } from '../useMemo';
17 |
18 | const useLintOptionChange = ({
19 | dispatchSetting,
20 | key,
21 | }: {
22 | dispatchSetting: DispatchSetting;
23 | key: keyof Setting['lintOption'];
24 | }) =>
25 | useCallback>(
26 | (_event, checked) =>
27 | dispatchSetting((prevSetting) => ({
28 | ...prevSetting,
29 | lintOption: {
30 | ...prevSetting.lintOption,
31 | [key]: checked,
32 | },
33 | })),
34 | [dispatchSetting, key]
35 | );
36 |
37 | const useModeDispatch = ({
38 | dispatchSetting,
39 | mode,
40 | }: {
41 | dispatchSetting: DispatchSetting;
42 | mode: Setting['mode'];
43 | }) =>
44 | useCallback(
45 | () =>
46 | dispatchSetting((prevSetting) => ({
47 | ...prevSetting,
48 | mode,
49 | })),
50 | [dispatchSetting, mode]
51 | );
52 |
53 | interface SettingDialogProps {
54 | dispatchSetting: DispatchSetting;
55 | open: DialogProps['open'];
56 | setting: Setting;
57 | memos: Memo[];
58 | onClose?: () => void;
59 | }
60 |
61 | const SettingDialog: React.FunctionComponent = React.memo(
62 | ({ dispatchSetting, open, setting, memos, onClose }) => {
63 | const handleProfessionalModeChange = useModeDispatch({ dispatchSetting, mode: 'professional' });
64 | const handleStandardModeChange = useModeDispatch({ dispatchSetting, mode: 'standard' });
65 |
66 | const handleGeneralNovelStyleJaChange = useLintOptionChange({
67 | dispatchSetting,
68 | key: 'generalNovelStyleJa',
69 | });
70 |
71 | const handleJaKyoikuKanjiChange = useLintOptionChange({
72 | dispatchSetting,
73 | key: 'jaKyoikuKanji',
74 | });
75 |
76 | const handleJaNoMixedPeriodChange = useLintOptionChange({
77 | dispatchSetting,
78 | key: 'jaNoMixedPeriod',
79 | });
80 |
81 | const handleJaNoWeakPhraseChange = useLintOptionChange({
82 | dispatchSetting,
83 | key: 'jaNoWeakPhrase',
84 | });
85 |
86 | const handleMaxAppearenceCountOfWordsChange = useLintOptionChange({
87 | dispatchSetting,
88 | key: 'maxAppearenceCountOfWords',
89 | });
90 |
91 | const handleNoFillerChange = useLintOptionChange({
92 | dispatchSetting,
93 | key: 'noFiller',
94 | });
95 |
96 | const handlePresetJaSpacingChange = useLintOptionChange({
97 | dispatchSetting,
98 | key: 'presetJaSpacing',
99 | });
100 |
101 | const handlePresetJaTechnicalWritingChange = useLintOptionChange({
102 | dispatchSetting,
103 | key: 'presetJaTechnicalWriting',
104 | });
105 |
106 | const handlePresetJTFStyleChange = useLintOptionChange({
107 | dispatchSetting,
108 | key: 'presetJTFStyle',
109 | });
110 |
111 | const handleUserDictionaryMemoIdChange = useCallback>(
112 | (event) => {
113 | const value = event.target.value as string;
114 | const userDictionaryMemoId = value === 'none' ? undefined : value;
115 |
116 | dispatchSetting((prevSetting) => ({
117 | ...prevSetting,
118 | lintOption: {
119 | ...prevSetting.lintOption,
120 | userDictionaryMemoId,
121 | },
122 | }));
123 | },
124 | [dispatchSetting]
125 | );
126 |
127 | return (
128 |
129 | 校正設定
130 |
131 |
132 |
133 | }
136 | label="スタンダードモード"
137 | onChange={handleStandardModeChange}
138 | />
139 |
140 |
141 | 校正さんの標準設定です。 自分で設定を決めていく必要はありません。
142 |
143 |
144 |
145 | }
148 | label="プロフェッショナルモード"
149 | onChange={handleProfessionalModeChange}
150 | />
151 |
152 |
153 | メモごとに校正したい項目を追加して、カスタマイズできます。
154 |
155 |
156 |
157 |
158 | }
161 | label="小説の一般的な作法"
162 | onChange={handleGeneralNovelStyleJaChange}
163 | />
164 |
165 | }
168 | label="技術文書"
169 | onChange={handlePresetJaTechnicalWritingChange}
170 | />
171 |
172 | }
175 | label="JTF日本語標準スタイルガイド(翻訳用)"
176 | onChange={handlePresetJTFStyleChange}
177 | />
178 |
179 | }
182 | label="弱い表現の禁止"
183 | onChange={handleJaNoWeakPhraseChange}
184 | />
185 |
186 | }
189 | label="単語の出現回数の上限"
190 | onChange={handleMaxAppearenceCountOfWordsChange}
191 | />
192 |
193 | }
196 | label="句点の統一"
197 | onChange={handleJaNoMixedPeriodChange}
198 | />
199 |
200 | }
203 | label="フィラーの禁止"
204 | onChange={handleNoFillerChange}
205 | />
206 |
207 | }
210 | label="スペースの統一"
211 | onChange={handlePresetJaSpacingChange}
212 | />
213 |
214 | }
217 | label="教育漢字のみ許可"
218 | onChange={handleJaKyoikuKanjiChange}
219 | />
220 |
221 |
222 |
223 |
229 | ユーザー辞書
230 |
235 | (無し)
236 | {memos.map((memo) => (
237 |
238 | {memo.text.trim().split('\n')[0]}
239 |
240 | ))}
241 |
242 |
243 |
244 |
245 | );
246 | }
247 | );
248 |
249 | export { SettingDialog };
250 |
--------------------------------------------------------------------------------
/packages/website/src/Edit/TextContainer/PinIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import FeedbackIcon from '@material-ui/icons/Feedback';
4 | import MuiInfoIcon from '@material-ui/icons/Info';
5 | import type { TextlintRuleSeverityLevel } from '@textlint/kernel';
6 |
7 | const ErrorIcon = styled(FeedbackIcon)`
8 | ${({ theme }) => `
9 | background-color: ${theme.palette.background.paper};
10 | `}
11 | opacity: 0.5;
12 | vertical-align: middle;
13 | `;
14 |
15 | const InfoIcon = styled(MuiInfoIcon)`
16 | ${({ theme }) => `
17 | background-color: ${theme.palette.background.paper};
18 | `}
19 | opacity: 0.5;
20 | vertical-align: middle;
21 | `;
22 |
23 | const PinIcon: React.FunctionComponent<{
24 | severity: TextlintRuleSeverityLevel;
25 | }> = React.memo(({ severity }) => {
26 | switch (severity) {
27 | case 0: {
28 | return ;
29 | }
30 |
31 | case 1: {
32 | throw new Error('severity is 1');
33 | }
34 |
35 | case 2: {
36 | return ;
37 | }
38 |
39 | default: {
40 | const exhaustiveCheck: never = severity;
41 |
42 | throw new Error(`Unknown severity: ${exhaustiveCheck}`);
43 | }
44 | }
45 | });
46 |
47 | export { PinIcon };
48 |
--------------------------------------------------------------------------------
/packages/website/src/Edit/TextContainer/TextContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useRef, useState } from 'react';
2 | import styled from 'styled-components';
3 | import Box from '@material-ui/core/Box';
4 | import Container from '@material-ui/core/Container';
5 | import IconButton from '@material-ui/core/IconButton';
6 | import List from '@material-ui/core/List';
7 | import ListItem from '@material-ui/core/ListItem';
8 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
9 | import ListItemText from '@material-ui/core/ListItemText';
10 | import Popover from '@material-ui/core/Popover';
11 | import Tooltip from '@material-ui/core/Tooltip';
12 | import Typography from '@material-ui/core/Typography';
13 | import SpellcheckIcon from '@material-ui/icons/Spellcheck';
14 | import * as Sentry from '@sentry/browser';
15 | import type { TextlintMessage, TextlintRuleSeverityLevel } from '@textlint/kernel';
16 | import type { Memo, MemosAction } from '../../useMemo';
17 | import { PinIcon } from './PinIcon';
18 |
19 | const removeExtraNewLine = (text: string) => (text === '\n' ? '' : text);
20 |
21 | const MessagePopover = styled(Popover)`
22 | word-break: break-all;
23 | `;
24 |
25 | const PinTarget = styled.div`
26 | cursor: pointer;
27 | padding: 8px;
28 | position: absolute;
29 | transform: translateY(-100%);
30 | `;
31 |
32 | const Content = styled.div`
33 | ${({ theme }) => `
34 | padding: ${theme.spacing(2)}px;
35 | &:empty::before {
36 | content: '校正する文章を入力';
37 | color: ${theme.palette.text.disabled};
38 | }
39 | `}
40 | outline: 0;
41 | word-break: break-all;
42 | `;
43 |
44 | interface LintMessage {
45 | index: TextlintMessage['index'];
46 | messages: TextlintMessage[];
47 | }
48 |
49 | interface Pin {
50 | left: React.CSSProperties['left'];
51 | message: LintMessage;
52 | top: React.CSSProperties['top'];
53 | }
54 |
55 | const getPins = ({
56 | lintMessages,
57 | text,
58 | textBoxRect,
59 | }: {
60 | lintMessages: LintMessage[];
61 | text: HTMLDivElement;
62 | textBoxRect: DOMRect;
63 | }) => {
64 | const pins: Pin[] = [];
65 | const range = document.createRange();
66 |
67 | for (const lintMessage of lintMessages) {
68 | let childNodesIndex = 0;
69 | let offset = lintMessage.index;
70 |
71 | for (childNodesIndex = 0; childNodesIndex < text.childNodes.length; childNodesIndex += 1) {
72 | const child = text.childNodes[childNodesIndex];
73 | const length =
74 | (child instanceof HTMLBRElement && 1) || (child instanceof Text && child.length);
75 |
76 | if (typeof length !== 'number') {
77 | return { reject: new Error('child.length is not defined') };
78 | }
79 |
80 | if (offset < length) {
81 | range.setStart(child, offset);
82 |
83 | break;
84 | }
85 |
86 | offset -= length;
87 | }
88 |
89 | if (childNodesIndex >= text.childNodes.length) {
90 | return { reject: new Error('childNodesIndex >= text.childNodes.length') };
91 | }
92 |
93 | const rangeRect = range.getBoundingClientRect();
94 |
95 | pins.push({
96 | left: rangeRect.left - textBoxRect.left - 8,
97 | message: lintMessage,
98 | top: rangeRect.top - textBoxRect.top + 8,
99 | });
100 | }
101 |
102 | return { resolve: pins };
103 | };
104 |
105 | const TextContainer: React.FunctionComponent<{
106 | dispatchIsLinting: React.Dispatch;
107 | dispatchIsTextContainerFocused: React.Dispatch>;
108 | dispatchMemos: React.Dispatch;
109 | isTextContainerFocused: boolean;
110 | memo: Memo;
111 | shouldDisplayResult: boolean;
112 | }> = React.memo(
113 | ({
114 | dispatchIsLinting,
115 | dispatchIsTextContainerFocused,
116 | dispatchMemos,
117 | isTextContainerFocused,
118 | memo,
119 | shouldDisplayResult,
120 | }) => {
121 | const [pins, setPins] = useState([]);
122 |
123 | const [popoverAnchorEl, setPopoverAnchorEl] = useState();
124 | const [popoverMessages, setPopoverMessages] = useState([]);
125 |
126 | const textRef = useRef(null);
127 | const textBoxRef = useRef(null);
128 |
129 | useEffect(() => {
130 | const dispatchText = () =>
131 | dispatchMemos((prevMemos) =>
132 | prevMemos.map((prevMemo) => {
133 | if (!textRef.current) {
134 | throw new Error('textRef.current is not defined');
135 | }
136 |
137 | return {
138 | ...prevMemo,
139 | ...(prevMemo.id === memo.id && {
140 | text: removeExtraNewLine(textRef.current.innerText),
141 | }),
142 | };
143 | })
144 | );
145 |
146 | window.addEventListener('beforeunload', dispatchText);
147 |
148 | return () => window.removeEventListener('beforeunload', dispatchText);
149 | }, [dispatchMemos, memo.id]);
150 |
151 | useEffect(() => {
152 | // Undo できるようにする。
153 | if (!textRef.current || textRef.current.innerText === memo.text) {
154 | return;
155 | }
156 |
157 | textRef.current.innerText = memo.text;
158 | }, [memo.text]);
159 |
160 | useEffect(() => {
161 | if (!memo.result) {
162 | return;
163 | }
164 |
165 | try {
166 | if (!textRef.current || !textBoxRef.current) {
167 | throw new Error('textRef.current or textBoxRef.current is not defined');
168 | }
169 |
170 | const mergedMessages: LintMessage[] = [];
171 |
172 | memo.result.messages.forEach((message) => {
173 | const duplicatedMessage = mergedMessages.find(({ index }) => index === message.index);
174 |
175 | if (duplicatedMessage) {
176 | duplicatedMessage.messages.push(message);
177 | } else {
178 | mergedMessages.push({
179 | ...message,
180 | messages: [message],
181 | });
182 | }
183 | });
184 |
185 | const getPinsResult = getPins({
186 | lintMessages: mergedMessages,
187 | text: textRef.current,
188 | textBoxRect: textBoxRef.current.getBoundingClientRect(),
189 | });
190 |
191 | if (getPinsResult.reject) {
192 | console.error(getPinsResult.reject);
193 | Sentry.captureException(getPinsResult.reject);
194 |
195 | return;
196 | } else {
197 | setPins(getPinsResult.resolve);
198 | }
199 | } finally {
200 | dispatchIsLinting(false);
201 | }
202 | }, [dispatchIsLinting, memo.result, memo.text]);
203 |
204 | const isPopoverOpen = Boolean(popoverAnchorEl);
205 |
206 | const handleFixClick = useCallback(
207 | ({ message }: { message: TextlintMessage }) => {
208 | if (!message.fix) {
209 | throw new Error('message.fix is not defined');
210 | }
211 |
212 | const { range, text } = message.fix;
213 |
214 | dispatchMemos((prevMemos) =>
215 | prevMemos.map((prevMemo) => ({
216 | ...prevMemo,
217 | ...(prevMemo.id === memo.id && {
218 | result: undefined,
219 | text: `${prevMemo.text.slice(0, range[0])}${text}${prevMemo.text.slice(range[1])}`,
220 | }),
221 | }))
222 | );
223 |
224 | setPopoverAnchorEl(undefined);
225 | },
226 | [dispatchMemos, memo.id, setPopoverAnchorEl]
227 | );
228 |
229 | const handlePinClick = useCallback(
230 | ({
231 | currentTarget,
232 | messages,
233 | }: {
234 | currentTarget: Element;
235 | messages: LintMessage['messages'];
236 | }) => {
237 | setPopoverAnchorEl(currentTarget);
238 | setPopoverMessages(messages);
239 | },
240 | [setPopoverAnchorEl, setPopoverMessages]
241 | );
242 |
243 | const handlePopoverClose = useCallback(
244 | () => setPopoverAnchorEl(undefined),
245 | [setPopoverAnchorEl]
246 | );
247 |
248 | const handleTextContainerBlur = useCallback(() => {
249 | dispatchIsTextContainerFocused(false);
250 |
251 | dispatchMemos((prevMemos) =>
252 | prevMemos.map((prevMemo) => {
253 | if (!textRef.current) {
254 | throw new Error('textRef.current is not defined');
255 | }
256 |
257 | return {
258 | ...prevMemo,
259 | ...(prevMemo.id === memo.id && {
260 | result: undefined,
261 | text: removeExtraNewLine(textRef.current.innerText),
262 | }),
263 | };
264 | })
265 | );
266 | }, [dispatchIsTextContainerFocused, dispatchMemos, memo.id]);
267 |
268 | const handleTextContainerFocus = useCallback(
269 | () => dispatchIsTextContainerFocused(true),
270 | [dispatchIsTextContainerFocused]
271 | );
272 |
273 | return (
274 |
275 |
282 | {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
283 | {(props: any) => (
284 | // eslint-disable-next-line react/jsx-props-no-spreading
285 |
286 |
287 |
294 |
295 |
296 | {shouldDisplayResult &&
297 | pins.map(({ left, message, top }) => {
298 | const severity = Math.max(
299 | ...message.messages.map((message) => message.severity)
300 | ) as TextlintRuleSeverityLevel;
301 |
302 | return (
303 |
{
307 | handlePinClick({ currentTarget, messages: message.messages });
308 | }}
309 | >
310 |
311 |
312 | );
313 | })}
314 |
315 |
328 |
329 |
330 | {popoverMessages.map((message, index) => (
331 |
332 |
333 |
334 |
335 | {message.fix && (
336 |
337 | handleFixClick({ message })}>
338 |
339 |
340 |
341 | )}
342 |
343 |
344 | ))}
345 |
346 |
347 |
348 |
349 | )}
350 |
351 |
352 | );
353 | }
354 | );
355 |
356 | export { TextContainer };
357 |
--------------------------------------------------------------------------------
/packages/website/src/Edit/TextContainer/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './TextContainer';
2 |
--------------------------------------------------------------------------------
/packages/website/src/Edit/index.ts:
--------------------------------------------------------------------------------
1 | export { Edit } from './Edit';
2 |
--------------------------------------------------------------------------------
/packages/website/src/Empty.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import Button from '@material-ui/core/Button';
4 | import Container from '@material-ui/core/Container';
5 | import Grid from '@material-ui/core/Grid';
6 | import Link from '@material-ui/core/Link';
7 | import Typography from '@material-ui/core/Typography';
8 |
9 | const EmptyContainer = styled(Container)`
10 | ${({ theme }) => `
11 | margin-bottom: ${theme.spacing(2)}px;
12 | margin-top: ${theme.spacing(2)}px;
13 | `}
14 | `;
15 |
16 | const Empty: React.FunctionComponent = React.memo(() => (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 文章作成をサポート
26 |
27 |
28 |
29 |
30 |
31 | 原稿やビジネス文書、個人のメモなどをきれいに残しましょう。
32 |
33 |
34 |
35 |
36 |
42 |
43 | メモを追加
44 |
45 |
46 |
47 |
48 |
49 | ));
50 |
51 | export default Empty;
52 |
--------------------------------------------------------------------------------
/packages/website/src/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import styled from 'styled-components';
3 | import Divider from '@material-ui/core/Divider';
4 | import Link from '@material-ui/core/Link';
5 | import List from '@material-ui/core/List';
6 | import ListItem from '@material-ui/core/ListItem';
7 | import ListItemIcon from '@material-ui/core/ListItemIcon';
8 | import ListItemText from '@material-ui/core/ListItemText';
9 | import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline';
10 | import FavoriteIcon from '@material-ui/icons/Favorite';
11 | import ForumIcon from '@material-ui/icons/Forum';
12 | import HelpIcon from '@material-ui/icons/Help';
13 | import NoteAddIcon from '@material-ui/icons/NoteAdd';
14 | import { v4 as uuidv4 } from 'uuid';
15 | import { initialSetting } from './useMemo';
16 | import type { Memo, MemosAction } from './useMemo';
17 |
18 | const DrawerContainer = styled.div`
19 | width: 250px;
20 | `;
21 |
22 | const MemoText = styled(ListItemText)`
23 | overflow: hidden;
24 | text-overflow: ellipsis;
25 | white-space: nowrap;
26 | `;
27 |
28 | export interface SidebarProps {
29 | dispatchMemoId: React.Dispatch;
30 | dispatchMemos: React.Dispatch;
31 | memoId: Memo['id'];
32 | memos: Memo[];
33 | onClose?: () => void;
34 | }
35 |
36 | const Sidebar: React.FunctionComponent = React.memo(
37 | ({ dispatchMemoId, dispatchMemos, memoId, memos, onClose }) => {
38 | const handleAddClick = useCallback(() => {
39 | const id = uuidv4();
40 |
41 | dispatchMemoId(id);
42 |
43 | dispatchMemos((prevMemos) => [
44 | ...prevMemos,
45 | {
46 | id,
47 | result: {
48 | filePath: '',
49 | messages: [],
50 | },
51 | setting: initialSetting,
52 | text: '',
53 | },
54 | ]);
55 |
56 | onClose?.();
57 | }, [dispatchMemoId, dispatchMemos, onClose]);
58 |
59 | const handleMemoClick = useCallback(
60 | (id: Memo['id']) => {
61 | dispatchMemoId(id);
62 |
63 | onClose?.();
64 | },
65 | [dispatchMemoId, onClose]
66 | );
67 |
68 | return (
69 |
70 |
71 | {memos.map(({ id, result, text }) => (
72 | handleMemoClick(id)} selected={id === memoId}>
73 | {result?.messages.length === 0 && }
74 |
75 |
76 |
77 | ))}
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | );
141 | }
142 | );
143 |
144 | export default Sidebar;
145 |
--------------------------------------------------------------------------------
/packages/website/src/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useMemo } from 'react';
2 | import type { FunctionComponent } from 'react';
3 | import {
4 | CssBaseline,
5 | ThemeProvider as MuiThemeProvider,
6 | createTheme,
7 | useMediaQuery,
8 | } from '@material-ui/core';
9 | import type { Theme } from '@material-ui/core';
10 | import { jaJP } from '@material-ui/core/locale';
11 | import { ThemeProvider as StyledThemeProvider } from 'styled-components';
12 |
13 | declare module 'styled-components' {
14 | // eslint-disable-next-line
15 | export interface DefaultTheme extends Theme { }
16 | }
17 |
18 | const ThemeProvider: FunctionComponent = memo(({ children }) => {
19 | const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
20 |
21 | const theme = useMemo(
22 | () =>
23 | createTheme(
24 | {
25 | palette: {
26 | type: prefersDarkMode ? 'dark' : 'light',
27 | primary: {
28 | main: '#00a39b',
29 | },
30 | secondary: {
31 | main: '#f15d69',
32 | },
33 | },
34 | typography: {
35 | fontFamily:
36 | '"Noto Sans CJK JP", "ヒラギノ角ゴシック Pro", "Hiragino Kaku Gothic Pro", "游ゴシック Medium", "Yu Gothic Medium", "Roboto", "Helvetica", "Arial", sans-serif',
37 | },
38 | },
39 | jaJP
40 | ),
41 | [prefersDarkMode]
42 | );
43 |
44 | return (
45 |
46 |
47 |
48 | {children}
49 |
50 |
51 | );
52 | });
53 |
54 | export { ThemeProvider };
55 |
--------------------------------------------------------------------------------
/packages/website/src/browserCheck.ts:
--------------------------------------------------------------------------------
1 | import supportedBrowsers from './supportedBrowsers.json';
2 | import { supportedBrowsersRegExp } from './supportedBrowsersRegExp';
3 |
4 | if (!supportedBrowsersRegExp.test(navigator.userAgent)) {
5 | alert(`校正さんを使用するには、下記のウェブブラウザからアクセスしてください。
6 |
7 | ${supportedBrowsers.browsers.map((browser) => `・${browser}`).join('\n')}
8 |
9 | ヘルプ https://helpfeel.com/hata6502/?q=%E6%96%87%E7%AB%A0%E6%A0%A1%E6%AD%A3
10 | `);
11 | }
12 |
--------------------------------------------------------------------------------
/packages/website/src/index.tsx:
--------------------------------------------------------------------------------
1 | import 'core-js';
2 | import 'regenerator-runtime/runtime';
3 |
4 | import React from 'react';
5 | import ReactDOM from 'react-dom';
6 | import { StylesProvider } from '@material-ui/core/styles';
7 | import * as Sentry from '@sentry/browser';
8 | import App from './App';
9 | import { ThemeProvider } from './ThemeProvider';
10 |
11 | const renderFatalError = ({ message }: { message: React.ReactNode }) =>
12 | ReactDOM.render(
13 | <>
14 | {message}
15 |
16 |
17 |
22 | ヘルプ
23 |
24 | >,
25 | document.querySelector('.app')
26 | );
27 |
28 | const main = () => {
29 | if (process.env.NODE_ENV === 'production') {
30 | Sentry.init({
31 | beforeSend: (event) => {
32 | if (event.exception) {
33 | Sentry.showReportDialog({ eventId: event.event_id });
34 | }
35 |
36 | return event;
37 | },
38 | dsn: 'https://c98bf237258047cb89f0b618d16bbf53@sentry.io/3239618',
39 | });
40 |
41 | if ('serviceWorker' in navigator) {
42 | window.addEventListener('load', () => navigator.serviceWorker.register('service-worker.js'));
43 | }
44 | }
45 |
46 | if (!window.Worker) {
47 | renderFatalError({ message: '校正さんを使用するには、Web Worker を有効にしてください。' });
48 |
49 | return;
50 | }
51 |
52 | const lintWorker = new Worker('lintWorker.js');
53 |
54 | try {
55 | const localStorageTest = 'localStorageTest';
56 |
57 | localStorage.setItem(localStorageTest, localStorageTest);
58 | localStorage.removeItem(localStorageTest);
59 | } catch (exception) {
60 | const unavailable =
61 | !(exception instanceof DOMException) ||
62 | (exception.code !== 22 &&
63 | exception.code !== 1014 &&
64 | exception.name !== 'QuotaExceededError' &&
65 | exception.name !== 'NS_ERROR_DOM_QUOTA_REACHED') ||
66 | !localStorage ||
67 | localStorage.length === 0;
68 |
69 | if (unavailable) {
70 | renderFatalError({ message: '校正さんを使用するには、localStorage を有効にしてください。' });
71 |
72 | return;
73 | }
74 | }
75 |
76 | ReactDOM.render(
77 |
78 |
79 |
80 |
81 | ,
82 | document.querySelector('.app')
83 | );
84 | };
85 |
86 | main();
87 |
--------------------------------------------------------------------------------
/packages/website/src/lintWorker.ts:
--------------------------------------------------------------------------------
1 | import 'core-js';
2 | import 'regenerator-runtime/runtime';
3 |
4 | import type { TextlintResult } from '@textlint/kernel';
5 | import { lint } from 'core';
6 | import type { LintOption } from 'core';
7 |
8 | declare global {
9 | interface Window {
10 | kuromojin?: {
11 | dicPath?: string;
12 | };
13 | }
14 | }
15 |
16 | // eslint-disable-next-line no-restricted-globals
17 | self.kuromojin = {
18 | dicPath: 'https://kohsei-san.hata6502.com/dict/',
19 | };
20 |
21 | interface LintWorkerLintMessage {
22 | lintOption: LintOption;
23 | text: string;
24 | }
25 |
26 | interface LintWorkerResultMessage {
27 | result: TextlintResult;
28 | text: string;
29 | }
30 |
31 | // eslint-disable-next-line no-restricted-globals
32 | addEventListener('message', async (event: MessageEvent) => {
33 | const text = event.data.text;
34 |
35 | const message: LintWorkerResultMessage = {
36 | result: await lint({ lintOption: event.data.lintOption, text }),
37 | text,
38 | };
39 |
40 | postMessage(message);
41 | });
42 |
43 | lint({ lintOption: {}, text: '初回校正時でもキャッシュにヒットさせるため。' });
44 |
45 | export type { LintWorkerLintMessage, LintWorkerResultMessage };
46 |
--------------------------------------------------------------------------------
/packages/website/src/supportedBrowsers.json:
--------------------------------------------------------------------------------
1 | {
2 | "browsers": [
3 | "and_chr 87",
4 | "chrome 87",
5 | "chrome 86",
6 | "chrome 85",
7 | "edge 87",
8 | "edge 86",
9 | "ios_saf 14.0-14.3",
10 | "ios_saf 13.4-13.7",
11 | "ios_saf 13.3",
12 | "ios_saf 13.2",
13 | "ios_saf 13.0-13.1",
14 | "safari 14",
15 | "safari 13.1",
16 | "safari 13"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/website/src/supportedBrowsersRegExp.ts:
--------------------------------------------------------------------------------
1 | const supportedBrowsersRegExp =
2 | /((CPU[ +]OS|iPhone[ +]OS|CPU[ +]iPhone|CPU IPhone OS)[ +]+(13[_.]0|13[_.]([1-9]|\d{2,})|13[_.]7|13[_.]([8-9]|\d{2,})|(1[4-9]|[2-9]\d|\d{3,})[_.]\d+|14[_.]0|14[_.]([1-9]|\d{2,})|14[_.]3|14[_.]([4-9]|\d{2,})|(1[5-9]|[2-9]\d|\d{3,})[_.]\d+)(?:[_.]\d+)?)|(Edge\/(86(?:\.0)?|86(?:\.([1-9]|\d{2,}))?|(8[7-9]|9\d|\d{3,})(?:\.\d+)?))|((Chromium|Chrome)\/(85\.0|85\.([1-9]|\d{2,})|(8[6-9]|9\d|\d{3,})\.\d+)(?:\.\d+)?)|(Version\/(13\.0|13\.([1-9]|\d{2,})|(1[4-9]|[2-9]\d|\d{3,})\.\d+|14\.0|14\.([1-9]|\d{2,})|(1[5-9]|[2-9]\d|\d{3,})\.\d+)(?:\.\d+)? Safari\/)/;
3 |
4 | export { supportedBrowsersRegExp };
5 |
--------------------------------------------------------------------------------
/packages/website/src/useMemo.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useReducer, useState } from 'react';
2 | import type { Dispatch, SetStateAction } from 'react';
3 | import * as Sentry from '@sentry/browser';
4 | import { TextlintResult } from '@textlint/kernel';
5 | import { v4 as uuidv4 } from 'uuid';
6 | import type { LintOption } from 'core';
7 |
8 | interface Setting {
9 | mode: 'standard' | 'professional';
10 | lintOption: LintOption & {
11 | userDictionaryMemoId?: string
12 | };
13 | }
14 |
15 | const initialSetting: Setting = {
16 | mode: 'standard',
17 | lintOption: {},
18 | };
19 |
20 | interface Memo {
21 | id: string;
22 | result?: TextlintResult;
23 | setting: Setting;
24 | text: string;
25 | }
26 |
27 | type MemosAction = (prevMemo: Memo[]) => Memo[];
28 |
29 | type DispatchSetting = (action: (prevSetting: Setting) => Setting) => void;
30 |
31 | const useDispatchSetting = ({
32 | dispatchMemos,
33 | memoId,
34 | }: {
35 | dispatchMemos: React.Dispatch;
36 | memoId: Memo['id'];
37 | }): DispatchSetting =>
38 | useCallback(
39 | (action) =>
40 | dispatchMemos((prevMemos) =>
41 | prevMemos.map((prevMemo) => ({
42 | ...prevMemo,
43 | ...(prevMemo.id === memoId && {
44 | result: undefined,
45 | setting: action(prevMemo.setting),
46 | }),
47 | }))
48 | ),
49 | [dispatchMemos, memoId]
50 | );
51 |
52 | const useMemo = (): {
53 | dispatchMemoId: Dispatch;
54 | dispatchMemos: Dispatch;
55 | isSaveErrorOpen: boolean;
56 | memoId: string;
57 | memos: Memo[];
58 | setIsSaveErrorOpen: Dispatch>;
59 | titleParam: string | null;
60 | } => {
61 | const searchParams = new URLSearchParams(window.location.search);
62 |
63 | const textParam = searchParams.get('text');
64 | const titleParam = searchParams.get('title');
65 | const urlParam = searchParams.get('url');
66 |
67 | const isShared = textParam !== null || titleParam !== null || urlParam !== null;
68 |
69 | const [memos, dispatchMemos] = useReducer(
70 | (state: Memo[], action: MemosAction) => action(state),
71 | undefined,
72 | (): Memo[] => {
73 | const memosItem = localStorage.getItem('memos');
74 | const localStorageMemos: Partial[] = memosItem ? JSON.parse(memosItem) : [];
75 |
76 | return [
77 | ...localStorageMemos.map(
78 | (localStorageMemo): Memo => ({
79 | id: uuidv4(),
80 | setting: initialSetting,
81 | text: '',
82 | ...localStorageMemo,
83 | })
84 | ),
85 | ...(isShared
86 | ? [
87 | {
88 | id: uuidv4(),
89 | setting: initialSetting,
90 | text: [
91 | ...(titleParam ? [titleParam] : []),
92 | ...(textParam ? [textParam] : []),
93 | ...(urlParam ? [urlParam] : []),
94 | ].join('\n\n'),
95 | },
96 | ]
97 | : []),
98 | ];
99 | }
100 | );
101 |
102 | const [memoId, dispatchMemoId] = useReducer(
103 | (_: Memo['id'], action: Memo['id']) => action,
104 | undefined,
105 | () => (isShared ? memos[memos.length - 1].id : localStorage.getItem('memoId') ?? '')
106 | );
107 |
108 | const [isSaveErrorOpen, setIsSaveErrorOpen] = useState(false);
109 |
110 | useEffect(() => {
111 | try {
112 | localStorage.setItem('memoId', memoId);
113 | localStorage.setItem('memos', JSON.stringify(memos));
114 | } catch (exception) {
115 | setIsSaveErrorOpen(true);
116 |
117 | if (
118 | !(exception instanceof DOMException) ||
119 | exception.code !== DOMException.QUOTA_EXCEEDED_ERR
120 | ) {
121 | console.error(exception);
122 | Sentry.captureException(exception);
123 | }
124 | }
125 | }, [memoId, memos]);
126 |
127 | return {
128 | dispatchMemoId,
129 | dispatchMemos,
130 | isSaveErrorOpen,
131 | memoId,
132 | memos,
133 | setIsSaveErrorOpen,
134 | titleParam,
135 | };
136 | };
137 |
138 | export { initialSetting, useDispatchSetting, useMemo };
139 | export type { DispatchSetting, Memo, MemosAction, Setting };
140 |
--------------------------------------------------------------------------------
/packages/website/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "esModuleInterop": true,
5 | "jsx": "react",
6 | "lib": ["DOM", "ESNext", "WebWorker"],
7 | "module": "ESNext",
8 | "moduleResolution": "node",
9 | "pretty": true,
10 | "resolveJsonModule": true,
11 | "skipLibCheck": true,
12 | "sourceMap": true,
13 | "strict": true,
14 | "target": "ESNext"
15 | },
16 | "include": ["src/**/*"],
17 | "exclude": ["node_modules", "docs"]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/website/webpack.browserCheck.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const path = require('path');
3 |
4 | module.exports = {
5 | mode: 'production',
6 | entry: {
7 | browserCheck: './src/browserCheck.ts',
8 | },
9 | module: {
10 | rules: [
11 | {
12 | test: /\.(j|t)sx?$/,
13 | loader: 'babel-loader',
14 | options: {
15 | presets: [
16 | [
17 | '@babel/preset-env',
18 | {
19 | targets: 'defaults',
20 | },
21 | ],
22 | '@babel/preset-typescript',
23 | ],
24 | },
25 | },
26 | ],
27 | },
28 | output: {
29 | path: path.resolve(__dirname, 'resources'),
30 | },
31 | resolve: {
32 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/packages/website/webpack.common.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const CopyPlugin = require('copy-webpack-plugin');
3 | const path = require('path');
4 | const staticFs = require('babel-plugin-static-fs');
5 |
6 | module.exports = {
7 | entry: {
8 | main: './src/index.tsx',
9 | lintWorker: './src/lintWorker.ts',
10 | },
11 | module: {
12 | rules: [
13 | {
14 | test: /\.[jt]sx?$/,
15 | loader: 'babel-loader',
16 | options: {
17 | plugins: [
18 | 'istanbul',
19 | [
20 | staticFs,
21 | {
22 | target: 'browser',
23 | dynamic: false,
24 | },
25 | ],
26 | ],
27 | presets: [
28 | [
29 | '@babel/preset-env',
30 | {
31 | corejs: 3,
32 | useBuiltIns: 'entry',
33 | },
34 | ],
35 | '@babel/preset-react',
36 | '@babel/preset-typescript',
37 | ],
38 | },
39 | },
40 | ],
41 | },
42 | output: {
43 | path: path.resolve(__dirname, 'docs'),
44 | },
45 | plugins: [new CopyPlugin([{ from: 'resources' }])],
46 | resolve: {
47 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
48 | },
49 | };
50 |
--------------------------------------------------------------------------------
/packages/website/webpack.dev.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const merge = require('webpack-merge');
3 | const path = require('path');
4 | const common = require('./webpack.common.js');
5 |
6 | module.exports = merge(common, {
7 | mode: 'development',
8 | devServer: {
9 | contentBase: path.join(__dirname, 'docs'),
10 | },
11 | devtool: 'eval-cheap-source-map',
12 | });
13 |
--------------------------------------------------------------------------------
/packages/website/webpack.prod.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | // eslint-disable-next-line import/no-extraneous-dependencies
3 | const merge = require('webpack-merge');
4 | const { GenerateSW } = require('workbox-webpack-plugin');
5 | const common = require('./webpack.common.js');
6 |
7 | module.exports = merge(common, {
8 | mode: 'production',
9 | plugins: [
10 | new GenerateSW({
11 | maximumFileSizeToCacheInBytes: 64 * 1024 * 1024,
12 | }),
13 | ],
14 | });
15 |
--------------------------------------------------------------------------------