├── .githooks └── pre-commit ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .mocharc.json ├── .node-version ├── LICENSE ├── README.md ├── index.html ├── netlify.toml ├── package.json ├── src ├── Deferred.ts ├── index.ts └── kuromojin.ts ├── test ├── kuromojin-test.ts └── tsconfig.json ├── tsconfig.json ├── vite.config.mjs ├── web ├── index.css ├── index.ts └── kuromoji.patch.js └── yarn.lock /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | npx --no-install lint-staged 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [push, pull_request] 3 | env: 4 | CI: true 5 | jobs: 6 | test: 7 | name: "Test on Node.js ${{ matrix.node-version }}" 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: [ 18, 20 ] 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@v2 15 | - name: setup Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: Install 20 | run: yarn install 21 | - name: Test 22 | run: yarn test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /lib 3 | /public/dict 4 | ### https://raw.github.com/github/gitignore/608690d6b9a78c2a003affc792e49a84905b3118/Node.gitignore 5 | 6 | # Logs 7 | logs 8 | *.log 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directory 31 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 32 | node_modules 33 | 34 | # Debug log from npm 35 | npm-debug.log 36 | 37 | 38 | ### https://raw.github.com/github/gitignore/608690d6b9a78c2a003affc792e49a84905b3118/Global/JetBrains.gitignore 39 | 40 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 41 | 42 | *.iml 43 | 44 | ## Directory-based project format: 45 | .idea/ 46 | # if you remove the above rule, at least ignore the following: 47 | 48 | # User-specific stuff: 49 | # .idea/workspace.xml 50 | # .idea/tasks.xml 51 | # .idea/dictionaries 52 | 53 | # Sensitive or high-churn files: 54 | # .idea/dataSources.ids 55 | # .idea/dataSources.xml 56 | # .idea/sqlDataSources.xml 57 | # .idea/dynamic.xml 58 | # .idea/uiDesigner.xml 59 | 60 | # Gradle: 61 | # .idea/gradle.xml 62 | # .idea/libraries 63 | 64 | # Mongo Explorer plugin: 65 | # .idea/mongoSettings.xml 66 | 67 | ## File-based project format: 68 | *.ipr 69 | *.iws 70 | 71 | ## Plugin-specific files: 72 | 73 | # IntelliJ 74 | out/ 75 | 76 | # mpeltonen/sbt-idea plugin 77 | .idea_modules/ 78 | 79 | # JIRA plugin 80 | atlassian-ide-plugin.xml 81 | 82 | # Crashlytics plugin (for Android Studio and IntelliJ) 83 | com_crashlytics_export_strings.xml 84 | crashlytics.properties 85 | crashlytics-build.properties 86 | 87 | # Local Netlify folder 88 | .netlify 89 | 90 | /public/dict/ 91 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "ts-node-test-register" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v18.19.0 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 azu 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kuromojin [![Actions Status: test](https://github.com/azu/kuromojin/workflows/test/badge.svg)](https://github.com/azu/kuromojin/actions?query=workflow%3A"test") 2 | 3 | Provide a high level wrapper for [kuromoji.js](https://github.com/takuyaa/kuromoji.js "kuromoji.js"). 4 | 5 | ## Features 6 | 7 | - Promise based API 8 | - Cache Layer 9 | - Fetch the dictionary at once 10 | - Return same tokens for same text 11 | 12 | ## Installation 13 | 14 | npm install kuromojin 15 | 16 | ## Online Playground 17 | 18 | 📝 Require [DecompressionStream](https://developer.mozilla.org/ja/docs/Web/API/DecompressionStream) supported browser 19 | 20 | - 21 | 22 | ## Usage 23 | 24 | Export two API. 25 | 26 | - `getTokenizer()` return `Promise` that is resolved with kuromoji.js's `tokenizer` instance. 27 | - `tokenize()` return `Promise` that is resolved with analyzed tokens. 28 | - The array and objects returned by `tokenize()` are read-only to ensure immutability and prevent modification of cached data. 29 | 30 | ```js 31 | import {tokenize, getTokenizer} from "kuromojin"; 32 | 33 | getTokenizer().then(tokenizer => { 34 | // kuromoji.js's `tokenizer` instance 35 | }); 36 | 37 | tokenize(text).then(tokens => { 38 | console.log(tokens) 39 | /* 40 | [ { 41 | word_id: 509800, // 辞書内での単語ID 42 | word_type: 'KNOWN', // 単語タイプ(辞書に登録されている単語ならKNOWN, 未知語ならUNKNOWN) 43 | word_position: 1, // 単語の開始位置 44 | surface_form: '黒文字', // 表層形 45 | pos: '名詞', // 品詞 46 | pos_detail_1: '一般', // 品詞細分類1 47 | pos_detail_2: '*', // 品詞細分類2 48 | pos_detail_3: '*', // 品詞細分類3 49 | conjugated_type: '*', // 活用型 50 | conjugated_form: '*', // 活用形 51 | basic_form: '黒文字', // 基本形 52 | reading: 'クロモジ', // 読み 53 | pronunciation: 'クロモジ' // 発音 54 | } ] 55 | */ 56 | }); 57 | ``` 58 | 59 | ### For browser/global options 60 | 61 | If `window.kuromojin.dicPath` is defined, kuromojin use it as default dict path. 62 | 63 | ```js 64 | import {getTokenizer} from "kuromojin"; 65 | // Affect all module that are used kuromojin. 66 | window.kuromojin = { 67 | dicPath: "https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict" 68 | }; 69 | // this `getTokenizer` function use "https://kuromojin.netlify.com/dict" 70 | getTokenizer(); 71 | // === 72 | getTokenizer({dicPath: "https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict"}) 73 | ``` 74 | 75 | :memo: Test dictionary URL 76 | 77 | - "https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict" 78 | - cdn dict for kuromoji.js 79 | - https://kuromojin.netlify.com/dict/*.dat.gz 80 | - example: https://kuromojin.netlify.com/dict/base.dat.gz 81 | 82 | ### Note: backward compatibility for <= 1.1.0 83 | 84 | kuromojin v1.1.0 export `tokenize` as default function. 85 | 86 | kuromojin v2.0.0 remove the default function. 87 | 88 | ```js 89 | import kuromojin from "kuromojin"; 90 | // kuromojin === tokenize 91 | ``` 92 | 93 | Recommended: use `import {tokenize} from "kuromojin"` instead of it 94 | 95 | ```js 96 | import {tokenize} from "kuromojin"; 97 | ``` 98 | 99 | ### Note: kuromoji version is pinned 100 | 101 | kuromojin pin kuromoji's version. 102 | 103 | It aim to dedupe kuromoji's dictionary. 104 | The dictionary is large and avoid to duplicated dictionary. 105 | 106 | ## Related 107 | 108 | - [azu/morpheme-match: match function that match token(形態素解析) with sentence.](https://github.com/azu/morpheme-match/tree/master) 109 | 110 | ## Tests 111 | 112 | npm test 113 | 114 | ## Contributing 115 | 116 | 1. Fork it! 117 | 2. Create your feature branch: `git checkout -b my-new-feature` 118 | 3. Commit your changes: `git commit -am 'Add some feature'` 119 | 4. Push to the branch: `git push origin my-new-feature` 120 | 5. Submit a pull request :D 121 | 122 | ## License 123 | 124 | MIT 125 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | kuromojin 6 | 7 | 14 | 15 | 16 |
17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # example netlify.toml 2 | [build] 3 | command = "npm run netlify" 4 | functions = "functions" 5 | publish = "dist" 6 | 7 | [[headers]] 8 | # Define which paths this specific [[headers]] block will cover. 9 | for = "/dict/*" 10 | [headers.values] 11 | Access-Control-Allow-Origin = "*" 12 | ## Uncomment to use this redirect for Single Page Applications like create-react-app. 13 | ## Not needed for static site generators. 14 | #[[redirects]] 15 | # from = "/*" 16 | # to = "/index.html" 17 | # status = 200 18 | 19 | ## (optional) Settings for Netlify Dev 20 | ## https://github.com/netlify/cli/blob/master/docs/netlify-dev.md#project-detection 21 | #[dev] 22 | # command = "yarn start" # Command to start your dev server 23 | # port = 3000 # Port that the dev server will be listening on 24 | # publish = "dist" # Folder with the static content for _redirect file 25 | 26 | ## more info on configuring this file: https://www.netlify.com/docs/netlify-toml-reference/ 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kuromojin", 3 | "version": "3.0.1", 4 | "description": "Provide a high level wrapper for kuromoji.js", 5 | "keywords": [ 6 | "kuromoji", 7 | "kuromoji.js", 8 | "promise" 9 | ], 10 | "homepage": "https://github.com/azu/kuromojin", 11 | "bugs": { 12 | "url": "https://github.com/azu/kuromojin/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/azu/kuromojin.git" 17 | }, 18 | "license": "MIT", 19 | "main": "lib/index.js", 20 | "files": [ 21 | "lib/", 22 | "src/" 23 | ], 24 | "types": "lib/index.d.ts", 25 | "directories": { 26 | "test": "test" 27 | }, 28 | "scripts": { 29 | "dev": "npm run cp-public && vite", 30 | "preview": "npm run cp-public && vite preview", 31 | "web:build": "npm run cp-public && vite build", 32 | "build": "cross-env NODE_ENV=production tsc -p .", 33 | "prepublish": "npm run --if-present build", 34 | "test": "mocha \"test/**/*.{js,ts}\"", 35 | "watch": "tsc -p . --watch", 36 | "cp-public": "mkdir -p public && cp -r node_modules/kuromoji/dict public/", 37 | "netlify": "npm run web:build", 38 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,css}\"", 39 | "prepare": "git config --local core.hooksPath .githooks" 40 | }, 41 | "dependencies": { 42 | "@kvs/env": "^2.1.3", 43 | "kuromoji": "0.1.2", 44 | "lru_map": "^0.4.1" 45 | }, 46 | "devDependencies": { 47 | "@types/mocha": "^10.0.6", 48 | "@types/node": "^20.10.6", 49 | "cross-env": "^7.0.3", 50 | "lint-staged": "^15.2.0", 51 | "mocha": "^10.2.0", 52 | "node-stdlib-browser": "^1.2.0", 53 | "prettier": "^3.1.1", 54 | "tiny-compressor": "^1.0.1", 55 | "ts-node": "^10.9.2", 56 | "ts-node-test-register": "^10.0.0", 57 | "typescript": "^5.3.3", 58 | "vite": "^5.0.11", 59 | "vite-plugin-node-stdlib-browser": "^0.2.1" 60 | }, 61 | "email": "azuciao@gmail.com", 62 | "prettier": { 63 | "singleQuote": false, 64 | "printWidth": 120, 65 | "tabWidth": 4, 66 | "trailingComma": "none" 67 | }, 68 | "lint-staged": { 69 | "*.{js,jsx,ts,tsx,css}": [ 70 | "prettier --write" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Deferred.ts: -------------------------------------------------------------------------------- 1 | // LICENSE : MIT 2 | "use strict"; 3 | export default class Deferred { 4 | promise: Promise; 5 | resolve!: (value: T) => void; 6 | reject!: (reason: any) => void; 7 | 8 | constructor() { 9 | this.promise = new Promise((resolve, reject) => { 10 | this.resolve = resolve; 11 | this.reject = reject; 12 | }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type { getTokenizerOption, Tokenizer, KuromojiToken } from "./kuromojin"; 2 | export { getTokenizer, tokenize } from "./kuromojin"; 3 | -------------------------------------------------------------------------------- /src/kuromojin.ts: -------------------------------------------------------------------------------- 1 | // LICENSE : MIT 2 | "use strict"; 3 | import path from "path"; 4 | import { LRUMap } from "lru_map"; 5 | import Deferred from "./Deferred"; 6 | // @ts-expect-error: no type definition 7 | import kuromoji from "kuromoji"; 8 | 9 | export type Tokenizer = { 10 | tokenize: (text: string) => KuromojiToken[]; 11 | tokenizeForSentence: (text: string) => KuromojiToken[]; 12 | }; 13 | export type KuromojiToken = { 14 | // 辞書内での単語ID 15 | word_id: number; 16 | // 単語タイプ(辞書に登録されている単語ならKNOWN; 未知語ならUNKNOWN) 17 | word_type: "KNOWN" | "UNKNOWN"; 18 | // 表層形 19 | surface_form: string; 20 | // 品詞 21 | pos: string; 22 | // 品詞細分類1 23 | pos_detail_1: string; 24 | // 品詞細分類2 25 | pos_detail_2: string; 26 | // 品詞細分類3 27 | pos_detail_3: string; 28 | // 活用型 29 | conjugated_type: string; 30 | // 活用形 31 | conjugated_form: string; 32 | // 基本形 33 | basic_form: string; 34 | // 読み 35 | reading: string; 36 | // 発音 37 | pronunciation: string; 38 | // 単語の開始位置 39 | word_position: number; 40 | }; 41 | type KuromojiWindow = Window & { 42 | kuromojin?: { 43 | dicPath?: string; 44 | }; 45 | }; 46 | const deferred = new Deferred(); 47 | const getNodeModuleDirPath = () => { 48 | // Node 49 | if (typeof process !== "undefined" && typeof process.env === "object" && process.env.KUROMOJIN_DIC_PATH) { 50 | return process.env.KUROMOJIN_DIC_PATH; 51 | } 52 | // Browser 53 | // if window.kuromojin.dicPath is defined, use it as default dict path. 54 | const maybeKuromojiWindow: KuromojiWindow | undefined = typeof window != "undefined" ? window : undefined; 55 | if ( 56 | typeof maybeKuromojiWindow !== "undefined" && 57 | typeof maybeKuromojiWindow.kuromojin === "object" && 58 | typeof maybeKuromojiWindow.kuromojin.dicPath === "string" 59 | ) { 60 | return maybeKuromojiWindow.kuromojin.dicPath; 61 | } 62 | const kuromojiDir = path.dirname(require.resolve("kuromoji")); 63 | return path.join(kuromojiDir, "..", "dict"); 64 | }; 65 | 66 | // cache for tokenizer 67 | let _tokenizer: null | Tokenizer = null; 68 | // lock boolean 69 | let isLoading = false; 70 | // cache for tokenize 71 | const tokenizeCacheMap = new LRUMap(10000); 72 | 73 | export type getTokenizerOption = { 74 | dicPath: string; 75 | // Cache by default 76 | // Default: false 77 | noCacheTokenize?: boolean; 78 | }; 79 | 80 | export function getTokenizer(options: getTokenizerOption = { dicPath: getNodeModuleDirPath() }): Promise { 81 | if (_tokenizer) { 82 | return Promise.resolve(_tokenizer); 83 | } 84 | if (isLoading) { 85 | return deferred.promise; 86 | } 87 | isLoading = true; 88 | // load dict 89 | kuromoji.builder(options).build(function (err: undefined | Error, tokenizer: Tokenizer) { 90 | if (err) { 91 | return deferred.reject(err); 92 | } 93 | _tokenizer = tokenizer; 94 | deferred.resolve(tokenizer); 95 | }); 96 | return deferred.promise; 97 | } 98 | 99 | export function tokenize(text: string, options?: getTokenizerOption): Promise[]>> { 100 | return getTokenizer(options).then((tokenizer) => { 101 | if (options?.noCacheTokenize) { 102 | return tokenizer.tokenizeForSentence(text); 103 | } else { 104 | const cache = tokenizeCacheMap.get(text); 105 | if (cache) { 106 | return cache; 107 | } 108 | const tokens = tokenizer.tokenizeForSentence(text); 109 | tokenizeCacheMap.set(text, tokens); 110 | return tokens; 111 | } 112 | }); 113 | } 114 | -------------------------------------------------------------------------------- /test/kuromojin-test.ts: -------------------------------------------------------------------------------- 1 | // LICENSE : MIT 2 | "use strict"; 3 | import assert from "assert"; 4 | // it is compatible check for <= 1.1.0 5 | import { getTokenizer, tokenize } from "../src"; 6 | 7 | describe("kuromojin", function () { 8 | context("many access at a time", function () { 9 | it("should return a.promise", function () { 10 | var promises = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((_num) => { 11 | return getTokenizer(); 12 | }); 13 | return Promise.all(promises).then((tokenizer) => { 14 | tokenizer.reduce((prev, current) => { 15 | assert(prev === current); 16 | return current; 17 | }); 18 | }); 19 | }); 20 | }); 21 | context("tokenize", function () { 22 | it("is alias to default", function () { 23 | var data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 24 | var promises = data.map((num) => { 25 | return tokenize(String(num)); 26 | }); 27 | return Promise.all(promises).then((texts) => { 28 | texts.forEach((results, index) => { 29 | let firstNode = results[0]; 30 | assert.strictEqual(firstNode.surface_form, String(index)); 31 | }); 32 | }); 33 | }); 34 | it("should return a.promise that resolve analyzed text", function () { 35 | var data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 36 | var promises = data.map((num) => { 37 | return tokenize(String(num)); 38 | }); 39 | return Promise.all(promises).then((texts) => { 40 | texts.forEach((results, index) => { 41 | let firstNode = results[0]; 42 | assert.strictEqual(firstNode.surface_form, String(index)); 43 | }); 44 | }); 45 | }); 46 | it("should tokenize sentence", function () { 47 | return tokenize("これは1文。これは2文。").then((tokens) => { 48 | const firstToken = tokens[0]; 49 | assert.strictEqual(firstToken.word_position, 1); 50 | const lastToken = tokens[tokens.length - 1]; 51 | assert.strictEqual(lastToken.word_position, 12); 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "noEmit": true 6 | }, 7 | "include": [ 8 | "../src/**/*", 9 | "./**/*" 10 | ] 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "newLine": "LF", 8 | "outDir": "./lib/", 9 | "target": "es5", 10 | "sourceMap": true, 11 | "declaration": true, 12 | "jsx": "preserve", 13 | "lib": [ 14 | "es2018", 15 | "dom" 16 | ], 17 | /* Strict Type-Checking Options */ 18 | "strict": true, 19 | /* Additional Checks */ 20 | /* Report errors on unused locals. */ 21 | "noUnusedLocals": true, 22 | /* Report errors on unused parameters. */ 23 | "noUnusedParameters": true, 24 | /* Report error when not all code paths in function return a value. */ 25 | "noImplicitReturns": true, 26 | /* Report errors for fallthrough cases in switch statement. */ 27 | "noFallthroughCasesInSwitch": true 28 | }, 29 | "include": [ 30 | "src/**/*" 31 | ], 32 | "exclude": [ 33 | ".git", 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /vite.config.mjs: -------------------------------------------------------------------------------- 1 | import nodePolyfills from 'vite-plugin-node-stdlib-browser' 2 | export default { 3 | plugins: [nodePolyfills()] 4 | }; 5 | -------------------------------------------------------------------------------- /web/index.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: grid; 3 | grid-template-columns: repeat(2, 1fr); 4 | gap: 10px; 5 | grid-auto-rows: minmax(800px, 1fr); 6 | } 7 | 8 | .mainLabel { 9 | display: block; 10 | } 11 | 12 | .main textarea { 13 | width: 100%; 14 | height: 100%; 15 | } 16 | 17 | /* Theme */ 18 | :root { 19 | --nc-font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, 20 | "Open Sans", "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 21 | --nc-font-mono: Consolas, monaco, "Ubuntu Mono", "Liberation Mono", "Courier New", Courier, monospace; 22 | 23 | /* Light theme */ 24 | --nc-tx-1: #000000; 25 | --nc-tx-2: #1a1a1a; 26 | --nc-bg-1: #ffffff; 27 | --nc-bg-2: #f6f8fa; 28 | --nc-bg-3: #e5e7eb; 29 | --nc-lk-1: #0070f3; 30 | --nc-lk-2: #0366d6; 31 | --nc-lk-tx: #ffffff; 32 | --nc-ac-1: #79ffe1; 33 | --nc-ac-tx: #0c4047; 34 | 35 | /* Dark theme */ 36 | --nc-d-tx-1: #ffffff; 37 | --nc-d-tx-2: #eeeeee; 38 | --nc-d-bg-1: #000000; 39 | --nc-d-bg-2: #111111; 40 | --nc-d-bg-3: #222222; 41 | --nc-d-lk-1: #3291ff; 42 | --nc-d-lk-2: #0070f3; 43 | --nc-d-lk-tx: #ffffff; 44 | --nc-d-ac-1: #7928ca; 45 | --nc-d-ac-tx: #ffffff; 46 | } 47 | 48 | @media (prefers-color-scheme: dark) { 49 | :root { 50 | --nc-tx-1: var(--nc-d-tx-1); 51 | --nc-tx-2: var(--nc-d-tx-2); 52 | --nc-bg-1: var(--nc-d-bg-1); 53 | --nc-bg-2: var(--nc-d-bg-2); 54 | --nc-bg-3: var(--nc-d-bg-3); 55 | --nc-lk-1: var(--nc-d-lk-1); 56 | --nc-lk-2: var(--nc-d-lk-2); 57 | --nc-lk-tx: var(--nc--dlk-tx); 58 | --nc-ac-1: var(--nc-d-ac-1); 59 | --nc-ac-tx: var(--nc--dac-tx); 60 | } 61 | } 62 | 63 | * { 64 | /* Reset margins and padding */ 65 | margin: 0; 66 | padding: 0; 67 | } 68 | 69 | address, 70 | area, 71 | article, 72 | aside, 73 | audio, 74 | blockquote, 75 | datalist, 76 | details, 77 | dl, 78 | fieldset, 79 | figure, 80 | form, 81 | input, 82 | iframe, 83 | img, 84 | meter, 85 | nav, 86 | ol, 87 | optgroup, 88 | option, 89 | output, 90 | p, 91 | pre, 92 | progress, 93 | ruby, 94 | section, 95 | table, 96 | textarea, 97 | ul, 98 | video { 99 | /* Margins for most elements */ 100 | margin-bottom: 1rem; 101 | } 102 | 103 | html, 104 | input, 105 | select, 106 | button { 107 | /* Set body font family and some finicky elements */ 108 | font-family: var(--nc-font-sans); 109 | } 110 | 111 | body { 112 | /* Center body in page */ 113 | margin: 0 auto; 114 | padding: 2rem; 115 | border-radius: 6px; 116 | overflow-x: hidden; 117 | word-break: break-word; 118 | overflow-wrap: break-word; 119 | background: var(--nc-bg-1); 120 | 121 | /* Main body text */ 122 | color: var(--nc-tx-2); 123 | font-size: 1.03rem; 124 | line-height: 1.5; 125 | } 126 | 127 | ::selection { 128 | /* Set background color for selected text */ 129 | background: var(--nc-ac-1); 130 | color: var(--nc-ac-tx); 131 | } 132 | 133 | h1, 134 | h2, 135 | h3, 136 | h4, 137 | h5, 138 | h6 { 139 | line-height: 1; 140 | color: var(--nc-tx-1); 141 | padding-top: 0.875rem; 142 | } 143 | 144 | h1, 145 | h2, 146 | h3 { 147 | color: var(--nc-tx-1); 148 | padding-bottom: 2px; 149 | margin-bottom: 8px; 150 | border-bottom: 1px solid var(--nc-bg-2); 151 | } 152 | 153 | h4, 154 | h5, 155 | h6 { 156 | margin-bottom: 0.3rem; 157 | } 158 | 159 | h1 { 160 | font-size: 2.25rem; 161 | } 162 | 163 | h2 { 164 | font-size: 1.85rem; 165 | } 166 | 167 | h3 { 168 | font-size: 1.55rem; 169 | } 170 | 171 | h4 { 172 | font-size: 1.25rem; 173 | } 174 | 175 | h5 { 176 | font-size: 1rem; 177 | } 178 | 179 | h6 { 180 | font-size: 0.875rem; 181 | } 182 | 183 | a { 184 | color: var(--nc-lk-1); 185 | } 186 | 187 | a:hover { 188 | color: var(--nc-lk-2); 189 | } 190 | 191 | abbr:hover { 192 | /* Set the '?' cursor while hovering an abbreviation */ 193 | cursor: help; 194 | } 195 | 196 | blockquote { 197 | padding: 1.5rem; 198 | background: var(--nc-bg-2); 199 | border-left: 5px solid var(--nc-bg-3); 200 | } 201 | 202 | abbr { 203 | cursor: help; 204 | } 205 | 206 | blockquote *:last-child { 207 | padding-bottom: 0; 208 | margin-bottom: 0; 209 | } 210 | 211 | header { 212 | background: var(--nc-bg-2); 213 | border-bottom: 1px solid var(--nc-bg-3); 214 | padding: 2rem 1.5rem; 215 | 216 | /* This sets the right and left margins to cancel out the body's margins. It's width is still the same, but the background stretches across the page's width. */ 217 | 218 | margin: -2rem calc(0px - (50vw - 50%)) 2rem; 219 | 220 | /* Shorthand for: 221 | 222 | margin-top: -2rem; 223 | margin-bottom: 2rem; 224 | 225 | margin-left: calc(0px - (50vw - 50%)); 226 | margin-right: calc(0px - (50vw - 50%)); */ 227 | 228 | padding-left: calc(50vw - 50%); 229 | padding-right: calc(50vw - 50%); 230 | } 231 | 232 | header h1, 233 | header h2, 234 | header h3 { 235 | padding-bottom: 0; 236 | border-bottom: 0; 237 | } 238 | 239 | header > *:first-child { 240 | margin-top: 0; 241 | padding-top: 0; 242 | } 243 | 244 | header > *:last-child { 245 | margin-bottom: 0; 246 | } 247 | 248 | a button, 249 | button, 250 | input[type="submit"], 251 | input[type="reset"], 252 | input[type="button"] { 253 | font-size: 1rem; 254 | display: inline-block; 255 | padding: 6px 12px; 256 | text-align: center; 257 | text-decoration: none; 258 | white-space: nowrap; 259 | background: var(--nc-lk-1); 260 | color: var(--nc-lk-tx); 261 | border: 0; 262 | border-radius: 4px; 263 | box-sizing: border-box; 264 | cursor: pointer; 265 | color: var(--nc-lk-tx); 266 | } 267 | 268 | a button[disabled], 269 | button[disabled], 270 | input[type="submit"][disabled], 271 | input[type="reset"][disabled], 272 | input[type="button"][disabled] { 273 | cursor: default; 274 | opacity: 0.5; 275 | 276 | /* Set the [X] cursor while hovering a disabled link */ 277 | cursor: not-allowed; 278 | } 279 | 280 | .button:focus, 281 | .button:enabled:hover, 282 | button:focus, 283 | button:enabled:hover, 284 | input[type="submit"]:focus, 285 | input[type="submit"]:enabled:hover, 286 | input[type="reset"]:focus, 287 | input[type="reset"]:enabled:hover, 288 | input[type="button"]:focus, 289 | input[type="button"]:enabled:hover { 290 | background: var(--nc-lk-2); 291 | } 292 | 293 | code, 294 | pre, 295 | kbd, 296 | samp { 297 | /* Set the font family for monospaced elements */ 298 | font-family: var(--nc-font-mono); 299 | } 300 | 301 | code, 302 | samp, 303 | kbd, 304 | pre { 305 | /* The main preformatted style. This is changed slightly across different cases. */ 306 | background: var(--nc-bg-2); 307 | border: 1px solid var(--nc-bg-3); 308 | border-radius: 4px; 309 | padding: 3px 6px; 310 | /* ↓ font-size is relative to containing element, so it scales for titles*/ 311 | font-size: 0.9em; 312 | } 313 | 314 | kbd { 315 | /* Makes the kbd element look like a keyboard key */ 316 | border-bottom: 3px solid var(--nc-bg-3); 317 | } 318 | 319 | pre { 320 | padding: 1rem 1.4rem; 321 | max-width: 100%; 322 | overflow: auto; 323 | } 324 | 325 | pre code { 326 | /* When is in a
, reset it's formatting to blend in */
327 |     background: inherit;
328 |     font-size: inherit;
329 |     color: inherit;
330 |     border: 0;
331 |     padding: 0;
332 |     margin: 0;
333 | }
334 | 
335 | code pre {
336 |     /* When 
 is in a , reset it's formatting to blend in */
337 |     display: inline;
338 |     background: inherit;
339 |     font-size: inherit;
340 |     color: inherit;
341 |     border: 0;
342 |     padding: 0;
343 |     margin: 0;
344 | }
345 | 
346 | details {
347 |     /* Make the 
look more "clickable" */ 348 | padding: 0.6rem 1rem; 349 | background: var(--nc-bg-2); 350 | border: 1px solid var(--nc-bg-3); 351 | border-radius: 4px; 352 | } 353 | 354 | summary { 355 | /* Makes the look more like a "clickable" link with the pointer cursor */ 356 | cursor: pointer; 357 | font-weight: bold; 358 | } 359 | 360 | details[open] { 361 | /* Adjust the
padding while open */ 362 | padding-bottom: 0.75rem; 363 | } 364 | 365 | details[open] summary { 366 | /* Adjust the
padding while open */ 367 | margin-bottom: 6px; 368 | } 369 | 370 | details[open] > *:last-child { 371 | /* Resets the bottom margin of the last element in the
while
is opened. This prevents double margins/paddings. */ 372 | margin-bottom: 0; 373 | } 374 | 375 | dt { 376 | font-weight: bold; 377 | } 378 | 379 | dd::before { 380 | /* Add an arrow to data table definitions */ 381 | content: "→ "; 382 | } 383 | 384 | hr { 385 | /* Reset the border of the
separator, then set a better line */ 386 | border: 0; 387 | border-bottom: 1px solid var(--nc-bg-3); 388 | margin: 1rem auto; 389 | } 390 | 391 | fieldset { 392 | margin-top: 1rem; 393 | padding: 2rem; 394 | border: 1px solid var(--nc-bg-3); 395 | border-radius: 4px; 396 | } 397 | 398 | legend { 399 | padding: auto 0.5rem; 400 | } 401 | 402 | table { 403 | /* border-collapse sets the table's elements to share borders, rather than floating as separate "boxes". */ 404 | border-collapse: collapse; 405 | width: 100%; 406 | } 407 | 408 | td, 409 | th { 410 | border: 1px solid var(--nc-bg-3); 411 | text-align: left; 412 | padding: 0.5rem; 413 | } 414 | 415 | th { 416 | background: var(--nc-bg-2); 417 | } 418 | 419 | tr:nth-child(even) { 420 | /* Set every other cell slightly darker. Improves readability. */ 421 | background: var(--nc-bg-2); 422 | } 423 | 424 | table caption { 425 | font-weight: bold; 426 | margin-bottom: 0.5rem; 427 | } 428 | 429 | textarea { 430 | /* Don't let the