├── .vscode └── settings.json ├── hello.md ├── _config.yml ├── .husky ├── post-commit ├── commit-msg └── pre-commit ├── docs ├── example.gif └── gramma-vim.gif ├── assets ├── banner.png ├── divider.png ├── banner-small.png ├── gramma-logo.png ├── gramma-text.png ├── css │ └── style.scss ├── gramma-logo.svg └── gramma-text.svg ├── src ├── context.js ├── index.d.ts ├── actions │ ├── saveNow.js │ ├── save.js │ ├── checkNonInteractively.js │ ├── configure.js │ └── checkInteractively.js ├── index.js ├── utils │ ├── equal.js │ ├── unzipFile.js │ ├── stripStyles.js │ ├── appLocation.js │ ├── stripStyles.test.js │ ├── downloadFile.js │ └── findUpSync.js ├── validators │ ├── rules.js │ └── languages.js ├── commands │ ├── debug.js │ ├── paths.js │ ├── config.js │ ├── commit.js │ ├── listen.js │ ├── init.js │ ├── check.js │ ├── server.js │ └── hook.js ├── boot │ ├── load.js │ └── prepareConfig.js ├── server │ ├── getServerPID.js │ ├── getServerInfo.js │ ├── showServerGUI.js │ ├── stopServer.js │ ├── installServer.js │ └── startServer.js ├── text-manipulation │ ├── replaceAll.d.ts │ ├── replace.js │ ├── replaceAll.js │ ├── replaceAll.test.js │ └── replace.test.js ├── prompts │ ├── confirmServerReinstall.js │ ├── confirmPort.js │ ├── confirmConfig.js │ ├── mainMenu.js │ ├── confirmInit.js │ ├── handleMistake.js │ └── handleSave.js ├── initialConfig.js ├── requests │ ├── checkViaAPI.d.ts │ ├── updates.js │ ├── checkWithFallback.js │ ├── checkViaAPI.js │ └── checkViaCmd.js ├── components │ ├── FixMenu.js │ ├── Mistake.js │ ├── FixMenu.test.js │ └── Mistake.test.js ├── cli.test.js └── cli.js ├── .eslintignore ├── .gitignore ├── .prettierrc ├── lib ├── findUpSync.mjs ├── prepareMarkdown.mjs └── package.json ├── tsconfig.json ├── .github └── ISSUE_TEMPLATE │ ├── question.md │ ├── documentation.md │ ├── feature_request.md │ └── bug_report.md ├── examples ├── api-plain.js ├── api-simple.js └── api-markdown.js ├── .npmignore ├── .eslintrc.json ├── .circleci └── config.yml ├── LICENSE.md ├── scripts ├── zipBinaries.js └── checkLanguagesSupport.js ├── CHANGELOG.md ├── data ├── rules.json └── languages.json ├── .gramma.json ├── package.json ├── _layouts └── default.html └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /hello.md: -------------------------------------------------------------------------------- 1 | Hello world! 2 | 3 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /.husky/post-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npx gramma hook cleanup 4 | -------------------------------------------------------------------------------- /docs/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caderek/gramma/HEAD/docs/example.gif -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | exec < /dev/tty 4 | 5 | npx gramma hook $1 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caderek/gramma/HEAD/assets/banner.png -------------------------------------------------------------------------------- /assets/divider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caderek/gramma/HEAD/assets/divider.png -------------------------------------------------------------------------------- /docs/gramma-vim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caderek/gramma/HEAD/docs/gramma-vim.gif -------------------------------------------------------------------------------- /assets/banner-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caderek/gramma/HEAD/assets/banner-small.png -------------------------------------------------------------------------------- /assets/gramma-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caderek/gramma/HEAD/assets/gramma-logo.png -------------------------------------------------------------------------------- /assets/gramma-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caderek/gramma/HEAD/assets/gramma-text.png -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | const context = { 2 | argv: null, 3 | } 4 | 5 | module.exports = context 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/prepareMarkdown.mjs 2 | src/utils/prepareMarkdown.js 3 | src/utils/findUpSync.js 4 | */**/*.d.ts -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .history 3 | .vscode 4 | example-responses 5 | example.txt 6 | coverage 7 | bin 8 | assets/css/*.css 9 | assets/css/*.css.map -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | import check = require("./requests/checkViaAPI") 2 | import replaceAll = require("./text-manipulation/replaceAll") 3 | export { check, replaceAll } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": false, 6 | "arrowParens": "always", 7 | "printWidth": 80 8 | } 9 | -------------------------------------------------------------------------------- /lib/findUpSync.mjs: -------------------------------------------------------------------------------- 1 | // esbuild findUpSync.mjs --bundle --outfile=src/utils/findUpSync.js --format=cjs --platform=node 2 | import { findUpSync } from "find-up" 3 | 4 | export default findUpSync 5 | -------------------------------------------------------------------------------- /lib/prepareMarkdown.mjs: -------------------------------------------------------------------------------- 1 | import * as builder from "annotatedtext-remark" 2 | 3 | const prepareMarkdown = (text) => JSON.stringify(builder.build(text)) 4 | 5 | export default prepareMarkdown 6 | -------------------------------------------------------------------------------- /src/actions/saveNow.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | 3 | const saveNow = async (text, filePath) => { 4 | fs.writeFileSync(filePath, text) 5 | console.clear() 6 | } 7 | 8 | module.exports = saveNow 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/index.js"], 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "outDir": "src/types" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require("isomorphic-fetch") 2 | 3 | const check = require("./requests/checkViaAPI") 4 | const replaceAll = require("./text-manipulation/replaceAll") 5 | 6 | module.exports = { 7 | check, 8 | replaceAll, 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: All questions that do not require changes to the codebase 4 | title: '' 5 | labels: help wanted 6 | assignees: caderek 7 | 8 | --- 9 | 10 | **How can I help you?** 11 | -------------------------------------------------------------------------------- /src/utils/equal.js: -------------------------------------------------------------------------------- 1 | const { deepEqual } = require("assert") 2 | 3 | const equal = (a, b) => { 4 | try { 5 | deepEqual(a, b) 6 | return true 7 | } catch (e) { 8 | return false 9 | } 10 | } 11 | 12 | module.exports = equal 13 | -------------------------------------------------------------------------------- /examples/api-plain.js: -------------------------------------------------------------------------------- 1 | const { check } = require("../src") 2 | 3 | const main = async () => { 4 | const response = await check(`Helo worlt!`, { 5 | markdown: true, 6 | }) 7 | 8 | console.dir(response, { depth: null }) 9 | } 10 | 11 | main() 12 | -------------------------------------------------------------------------------- /examples/api-simple.js: -------------------------------------------------------------------------------- 1 | const { check } = require("../src") 2 | 3 | const main = async () => { 4 | const { language, matches } = await check("Some wrongg text to check.") 5 | 6 | console.log({ lang: language.name, mistakes: matches.length }) 7 | } 8 | 9 | main() 10 | -------------------------------------------------------------------------------- /src/validators/rules.js: -------------------------------------------------------------------------------- 1 | const rules = require("../../data/rules.json") 2 | 3 | const ruleOptions = rules.map((rule) => rule.id.toLowerCase()) 4 | 5 | const isRule = (value) => { 6 | return ruleOptions.includes(value) 7 | } 8 | 9 | module.exports = { 10 | ruleOptions, 11 | isRule, 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: All docs related issues 4 | title: '' 5 | labels: documentation 6 | assignees: caderek 7 | 8 | --- 9 | 10 | **Describe what is missing, unclear or incorrect** 11 | A clear and concise description of what you want us to change/add. 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | _layouts 2 | node_modules 3 | .history 4 | .circleci 5 | example-responses 6 | example.txt 7 | coverage 8 | bin 9 | .husky 10 | .github 11 | .vscode 12 | lib 13 | assets/css 14 | assets/gramma-logo.svg 15 | assets/gramma-text.svg 16 | assets/banner.png 17 | assets/banner-small.png 18 | scripts 19 | examples -------------------------------------------------------------------------------- /examples/api-markdown.js: -------------------------------------------------------------------------------- 1 | const { check } = require("../src") 2 | 3 | const main = async () => { 4 | const { language, matches } = await check(`Helo worlt!`, { 5 | markdown: true, 6 | }) 7 | 8 | console.log({ lang: language.name, mistakes: matches.length }) 9 | } 10 | 11 | main() 12 | -------------------------------------------------------------------------------- /src/commands/debug.js: -------------------------------------------------------------------------------- 1 | const debug = async (argv, cfg) => { 2 | console.log("config:") 3 | console.log(cfg) 4 | console.log("------------------------------------") 5 | console.log("argv:") 6 | console.log(argv) 7 | console.log("------------------------------------") 8 | } 9 | 10 | module.exports = debug 11 | -------------------------------------------------------------------------------- /src/utils/unzipFile.js: -------------------------------------------------------------------------------- 1 | const decompress = require("decompress") 2 | const decompressUnzip = require("decompress-unzip") 3 | 4 | const unzipFile = (pathToFile, outputFolder) => { 5 | return decompress(pathToFile, outputFolder, { 6 | plugins: [decompressUnzip()], 7 | }) 8 | } 9 | 10 | module.exports = unzipFile 11 | -------------------------------------------------------------------------------- /src/boot/load.js: -------------------------------------------------------------------------------- 1 | const prepareConfig = require("./prepareConfig") 2 | 3 | const load = (action) => (argv) => { 4 | if (argv.file && argv.file.endsWith(".md")) { 5 | argv.markdown = true // eslint-disable-line 6 | } 7 | 8 | const cfg = prepareConfig(argv) 9 | action(argv, cfg) 10 | } 11 | 12 | module.exports = load 13 | -------------------------------------------------------------------------------- /src/server/getServerPID.js: -------------------------------------------------------------------------------- 1 | const kleur = require("kleur") 2 | 3 | const getServerPID = (cfg) => { 4 | if (cfg.global.server_pid) { 5 | console.log(kleur.green(`API server PID: ${cfg.global.server_pid}`)) 6 | } else { 7 | console.log(kleur.yellow("API server is not running!")) 8 | } 9 | } 10 | 11 | module.exports = getServerPID 12 | -------------------------------------------------------------------------------- /src/commands/paths.js: -------------------------------------------------------------------------------- 1 | const appLocation = require("../utils/appLocation") 2 | 3 | const paths = (argv, cfg) => { 4 | console.log(`Global config: ${cfg.paths.globalConfigFile}`) 5 | console.log(`App location: ${appLocation}`) 6 | console.log(`Local server: ${cfg.global.server_path || "not installed"}`) 7 | } 8 | 9 | module.exports = paths 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier"], 3 | "rules": { 4 | "no-console": 0, 5 | "arrow-body-style": 0, 6 | "no-restricted-syntax": 0, 7 | "no-await-in-loop": 0, 8 | "camelcase": 0 9 | }, 10 | "env": { 11 | "jest": true, 12 | "node": true 13 | }, 14 | "globals": { 15 | "fetch": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/text-manipulation/replaceAll.d.ts: -------------------------------------------------------------------------------- 1 | export = replaceAll 2 | /** 3 | * Modifies provided text with specified transformations. 4 | * 5 | * @param text base text 6 | * @param transformations descriptions of changes to the text 7 | */ 8 | declare function replaceAll( 9 | text: string, 10 | transformations: { offset: number; length: number; change: string }[], 11 | ): string 12 | -------------------------------------------------------------------------------- /src/validators/languages.js: -------------------------------------------------------------------------------- 1 | const languages = require("../../data/languages.json") 2 | 3 | const languageOptions = [ 4 | "config", 5 | "auto", 6 | ...languages.map((language) => language.longCode), 7 | ] 8 | 9 | const isLanguage = (value) => { 10 | return languageOptions.includes(value) 11 | } 12 | 13 | module.exports = { 14 | languageOptions, 15 | isLanguage, 16 | } 17 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lib", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "annotatedtext-remark": "^1.0.1", 14 | "find-up": "^6.2.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/prompts/confirmServerReinstall.js: -------------------------------------------------------------------------------- 1 | const prompts = require("prompts") 2 | 3 | const confirmServerReinstall = () => { 4 | return prompts([ 5 | { 6 | type: "confirm", 7 | name: "reinstall", 8 | message: "Server already installed. Do you want to reinstall?", 9 | initial: true, 10 | }, 11 | ]) 12 | } 13 | 14 | module.exports = confirmServerReinstall 15 | -------------------------------------------------------------------------------- /src/initialConfig.js: -------------------------------------------------------------------------------- 1 | const { ruleOptions } = require("./validators/rules") 2 | 3 | const rules = {} 4 | 5 | ruleOptions.forEach((rule) => { 6 | rules[rule] = true 7 | }) 8 | 9 | const initialConfig = { 10 | api_url: "https://api.languagetool.org/v2/check", 11 | api_key: "", 12 | dictionary: [], 13 | language: "en-US", 14 | rules, 15 | } 16 | 17 | module.exports = initialConfig 18 | -------------------------------------------------------------------------------- /src/prompts/confirmPort.js: -------------------------------------------------------------------------------- 1 | const prompts = require("prompts") 2 | 3 | const confirmPort = () => { 4 | return prompts([ 5 | { 6 | type: "toggle", 7 | name: "autoPort", 8 | message: "Port is in use, should I automatically find another port?", 9 | initial: true, 10 | active: "yes", 11 | inactive: "no", 12 | }, 13 | ]) 14 | } 15 | 16 | module.exports = confirmPort 17 | -------------------------------------------------------------------------------- /src/text-manipulation/replace.js: -------------------------------------------------------------------------------- 1 | const replace = (text, change, offset, length) => { 2 | const before = text.slice(0, offset) 3 | const mistake = text.slice(offset, offset + length) 4 | const after = text.slice(offset + length) 5 | 6 | const newPhrase = typeof change === "function" ? change(mistake) : change 7 | 8 | return `${before}${newPhrase}${after}` 9 | } 10 | 11 | module.exports = replace 12 | -------------------------------------------------------------------------------- /src/utils/stripStyles.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Strips terminal styles from string 3 | * (colors, weight etc.) 4 | * 5 | * @param {string} text any string 6 | */ 7 | const stripStyles = (text) => { 8 | // eslint-disable-next-line no-control-regex 9 | const regex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g 10 | 11 | return text.replace(regex, "") 12 | } 13 | 14 | module.exports = stripStyles 15 | -------------------------------------------------------------------------------- /src/prompts/confirmConfig.js: -------------------------------------------------------------------------------- 1 | const prompts = require("prompts") 2 | 3 | const confirmConfig = () => { 4 | return prompts([ 5 | { 6 | type: "toggle", 7 | name: "useGlobal", 8 | message: 9 | "Local config not found. Should I use the global config instead?", 10 | initial: true, 11 | active: "yes", 12 | inactive: "no", 13 | }, 14 | ]) 15 | } 16 | 17 | module.exports = confirmConfig 18 | -------------------------------------------------------------------------------- /src/utils/appLocation.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const fs = require("fs") 3 | 4 | const binDir = path.dirname(process.execPath) 5 | const scriptDir = __dirname 6 | 7 | let appLocation 8 | 9 | if (scriptDir.includes("snapshot")) { 10 | const executable = fs.readdirSync(binDir)[0] 11 | appLocation = path.resolve(binDir, executable) 12 | } else { 13 | appLocation = path.resolve(scriptDir, "..", "cli.js") 14 | } 15 | 16 | module.exports = appLocation 17 | -------------------------------------------------------------------------------- /src/server/getServerInfo.js: -------------------------------------------------------------------------------- 1 | const kleur = require("kleur") 2 | 3 | const getServerInfo = (cfg) => { 4 | if (cfg.global.server_pid) { 5 | console.log(kleur.green("PID: "), kleur.white(cfg.global.server_pid)) 6 | console.log(kleur.green("Url: "), kleur.white(cfg.global.api_url)) 7 | console.log(kleur.green("Path:"), kleur.white(cfg.global.server_path)) 8 | } else { 9 | console.log(kleur.yellow("API server is not running!")) 10 | } 11 | } 12 | 13 | module.exports = getServerInfo 14 | -------------------------------------------------------------------------------- /src/text-manipulation/replaceAll.js: -------------------------------------------------------------------------------- 1 | const replace = require("./replace") 2 | 3 | /** 4 | * Modifies provided text with specified transformations. 5 | * 6 | * @param text base text 7 | * @param transformations descriptions of changes to the text 8 | */ 9 | const replaceAll = (text, transformations) => { 10 | return transformations 11 | .sort((a, b) => b.offset - a.offset) 12 | .reduce((previousText, { change, offset, length }) => { 13 | return replace(previousText, change, offset, length) 14 | }, text) 15 | } 16 | 17 | module.exports = replaceAll 18 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: cimg/node:14.18.0 6 | 7 | working_directory: ~/repo 8 | 9 | steps: 10 | - checkout 11 | 12 | - restore_cache: 13 | keys: 14 | - v1-dependencies-{{ checksum "package.json" }} 15 | - v1-dependencies- 16 | 17 | - run: yarn install 18 | 19 | - save_cache: 20 | paths: 21 | - node_modules 22 | key: v1-dependencies-{{ checksum "package.json" }} 23 | 24 | - run: yarn run test:ci 25 | - run: yarn run lint 26 | -------------------------------------------------------------------------------- /src/prompts/mainMenu.js: -------------------------------------------------------------------------------- 1 | const prompts = require("prompts") 2 | 3 | const mainMenu = () => { 4 | const choices = [ 5 | { title: "check file", value: "check" }, 6 | { title: "check text", value: "listen" }, 7 | ] 8 | 9 | return prompts([ 10 | { 11 | type: "select", 12 | name: "saveOption", 13 | message: "What do you want to do?", 14 | choices, 15 | }, 16 | { 17 | type: (prev) => (prev === "check" ? "text" : null), 18 | name: "fileName", 19 | message: "Chose file path (relative or absolute)", 20 | }, 21 | ]) 22 | } 23 | 24 | module.exports = mainMenu 25 | -------------------------------------------------------------------------------- /src/commands/config.js: -------------------------------------------------------------------------------- 1 | const kleur = require("kleur") 2 | const configure = require("../actions/configure") 3 | const confirmConfig = require("../prompts/confirmConfig") 4 | 5 | const config = async (argv, cfg) => { 6 | if (!argv.global && !cfg.paths.localConfigFile) { 7 | const { useGlobal } = await confirmConfig() 8 | 9 | if (useGlobal) { 10 | argv.global = true // eslint-disable-line 11 | } else { 12 | console.log(kleur.yellow("Aborting")) 13 | process.exit() 14 | } 15 | } 16 | 17 | configure(argv.key, argv.value, cfg, argv.global) 18 | console.log(kleur.green("Done!")) 19 | } 20 | 21 | module.exports = config 22 | -------------------------------------------------------------------------------- /src/text-manipulation/replaceAll.test.js: -------------------------------------------------------------------------------- 1 | const replaceAll = require("./replaceAll") 2 | 3 | describe("Replace all", () => { 4 | it.only("changes all places according to provided transformations", () => { 5 | const text = "Foo CHANGE_ONE baz CHANGE_TWO." 6 | const transformations = [ 7 | { 8 | offset: 4, 9 | length: 10, 10 | change: "bar", 11 | }, 12 | { 13 | offset: 19, 14 | length: 10, 15 | change: "bat", 16 | }, 17 | ] 18 | 19 | const expected = "Foo bar baz bat." 20 | const result = replaceAll(text, transformations) 21 | 22 | expect(result).toEqual(expected) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: caderek 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: caderek 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Desktop (please complete the following information):** 23 | - OS: [e.g. Linux, macOS, Windows] 24 | - Browser [e.g. Ubunto 18.04, Mojave, 10] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /src/commands/commit.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const { execSync } = require("child_process") 3 | const path = require("path") 4 | const checkInteractively = require("../actions/checkInteractively") 5 | 6 | const commit = async (argv, cfg) => { 7 | const { text } = await checkInteractively(argv.text, cfg) 8 | 9 | try { 10 | if (fs.existsSync(path.join(process.cwd(), ".gramma.json"))) { 11 | execSync(`git add .gramma.json`) 12 | } 13 | 14 | const output = argv.all 15 | ? execSync(`git commit -am "${text}"`) 16 | : execSync(`git commit -m "${text}"`) 17 | 18 | process.stdout.write(output) 19 | } catch (error) { 20 | process.stderr.write(error.stdout) 21 | } 22 | process.exit() 23 | } 24 | 25 | module.exports = commit 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021 Maciej Cąderek 2 | 3 | Permission to use, copy, modify, and/or distribute this software 4 | for any purpose with or without fee is hereby granted, 5 | provided that the above copyright notice 6 | and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 10 | OF MERCHANTABILITY AND FITNESS. 11 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, 12 | OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, 13 | DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, 14 | NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /src/prompts/confirmInit.js: -------------------------------------------------------------------------------- 1 | const prompts = require("prompts") 2 | const initialConfig = require("../initialConfig") 3 | 4 | const confirmInit = (hasGit) => { 5 | return prompts([ 6 | { 7 | type: "select", 8 | name: "api", 9 | message: "Choose API url:", 10 | choices: [ 11 | { title: "languagetool.org", value: initialConfig.api_url }, 12 | { 13 | title: "Inherit from global config", 14 | value: "inherit", 15 | }, 16 | ], 17 | initial: 0, 18 | }, 19 | { 20 | type: hasGit ? "toggle" : null, 21 | name: "hook", 22 | message: "Add Git hook?", 23 | initial: true, 24 | active: "yes", 25 | inactive: "no", 26 | }, 27 | ]) 28 | } 29 | 30 | module.exports = confirmInit 31 | -------------------------------------------------------------------------------- /src/commands/listen.js: -------------------------------------------------------------------------------- 1 | const intercept = require("intercept-stdout") 2 | const checkNonInteractively = require("../actions/checkNonInteractively") 3 | const checkInteractively = require("../actions/checkInteractively") 4 | const save = require("../actions/save") 5 | const stripStyles = require("../utils/stripStyles") 6 | 7 | const listen = async (argv, cfg) => { 8 | if (argv.print) { 9 | const noColors = argv["no-colors"] 10 | 11 | if (noColors) { 12 | intercept(stripStyles) 13 | } 14 | 15 | const status = await checkNonInteractively(argv.text, cfg, !noColors) 16 | process.exit(status) 17 | } else { 18 | const { changed, text } = await checkInteractively(argv.text, cfg) 19 | if (changed) { 20 | await save(text, "TEXT") 21 | } 22 | process.exit() 23 | } 24 | } 25 | 26 | module.exports = listen 27 | -------------------------------------------------------------------------------- /src/server/showServerGUI.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require("child_process") 2 | const path = require("path") 3 | const kleur = require("kleur") 4 | 5 | const showServerGUI = async (cfg) => { 6 | if (!cfg.global.server_path) { 7 | console.log( 8 | kleur.red(`Please install local server via: gramma server install`), 9 | ) 10 | return false 11 | } 12 | 13 | console.log("Starting local server GUI...") 14 | 15 | const command = "java" 16 | 17 | const params = ["-jar", path.join(cfg.global.server_path, "languagetool.jar")] 18 | 19 | const gui = spawn(command, params, { windowsHide: true, detached: true }) 20 | 21 | gui.on("error", (error) => { 22 | if (error) { 23 | console.log(kleur.red("Cannot start local server GUI automatically.")) 24 | process.exit(1) 25 | } 26 | }) 27 | 28 | return true 29 | } 30 | 31 | module.exports = showServerGUI 32 | -------------------------------------------------------------------------------- /src/text-manipulation/replace.test.js: -------------------------------------------------------------------------------- 1 | const replace = require("./replace") 2 | 3 | describe("Replace", () => { 4 | it("changes specified part of the text with provides word/phrase", () => { 5 | const text = "Foo CHANGE_ME baz." 6 | const change = "bar" 7 | const offset = 4 8 | const length = 9 9 | 10 | const expected = "Foo bar baz." 11 | const result = replace(text, change, offset, length) 12 | 13 | expect(result).toEqual(expected) 14 | }) 15 | 16 | it("changes specified part of the text according to provided function", () => { 17 | const text = "Foo CHANGE_ME baz." 18 | const change = (mistake) => mistake.toLowerCase() 19 | const offset = 4 20 | const length = 9 21 | 22 | const expected = "Foo change_me baz." 23 | const result = replace(text, change, offset, length) 24 | 25 | expect(result).toEqual(expected) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/utils/stripStyles.test.js: -------------------------------------------------------------------------------- 1 | const kleur = require("kleur") 2 | const stripStyles = require("./stripStyles") 3 | 4 | describe("Strips styles from console string", () => { 5 | it("strips colors", () => { 6 | const input = kleur.red("foo") 7 | const expected = "foo" 8 | const result = stripStyles(input) 9 | 10 | expect(result).toEqual(expected) 11 | }) 12 | 13 | it("strips background colors", () => { 14 | const input = kleur.bgRed("foo") 15 | const expected = "foo" 16 | const result = stripStyles(input) 17 | 18 | expect(result).toEqual(expected) 19 | }) 20 | 21 | it("strips font style", () => { 22 | const input = kleur 23 | .bold() 24 | .italic() 25 | .underline() 26 | .strikethrough() 27 | .dim("foo") 28 | const expected = "foo" 29 | const result = stripStyles(input) 30 | 31 | expect(result).toEqual(expected) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/commands/init.js: -------------------------------------------------------------------------------- 1 | const kleur = require("kleur") 2 | const fs = require("fs") 3 | const path = require("path") 4 | const initialConfig = require("../initialConfig") 5 | const confirmInit = require("../prompts/confirmInit") 6 | const { addHookCode, checkGit } = require("./hook") 7 | 8 | const localConfigFile = path.join(process.cwd(), ".gramma.json") 9 | 10 | const init = async (argv, cfg) => { 11 | if (!fs.existsSync(cfg.paths.localConfigFile)) { 12 | const hasGit = checkGit() 13 | const { hook, api } = await confirmInit(hasGit) 14 | 15 | if (!api) { 16 | console.log(kleur.yellow("Aborting!")) 17 | process.exit(1) 18 | } 19 | 20 | const content = JSON.stringify({ ...initialConfig, api_url: api }, null, 2) 21 | 22 | fs.writeFileSync(localConfigFile, content) 23 | console.log(kleur.green("Gramma config created!")) 24 | 25 | if (hook) { 26 | addHookCode(true) 27 | } 28 | } else { 29 | console.log(kleur.red("Gramma config already exists for this project!")) 30 | } 31 | } 32 | 33 | module.exports = init 34 | -------------------------------------------------------------------------------- /src/prompts/handleMistake.js: -------------------------------------------------------------------------------- 1 | const prompts = require("prompts") 2 | const FixMenu = require("../components/FixMenu") 3 | 4 | const handleMistake = (fixes, issue) => { 5 | console.log("---------------------------------") 6 | 7 | const dictionaryOptions = issue === "misspelling" ? ["l", "g"] : [] 8 | const validInputs = [ 9 | ...fixes.map((_, index) => String(index + 1)), 10 | "0", 11 | "i", 12 | ...dictionaryOptions, 13 | "n", 14 | ] 15 | 16 | const initialInput = fixes.length > 0 ? "1" : "0" 17 | 18 | return prompts([ 19 | { 20 | type: "text", 21 | name: "option", 22 | message: FixMenu(fixes, issue), 23 | initial: initialInput, 24 | validate(input) { 25 | return validInputs.includes(input) 26 | ? true 27 | : `Please enter a valid option...` 28 | }, 29 | }, 30 | { 31 | type: (prev) => (prev === "0" ? "text" : null), 32 | name: "replacement", 33 | message: "Provide replacement", 34 | }, 35 | ]) 36 | } 37 | 38 | module.exports = handleMistake 39 | -------------------------------------------------------------------------------- /src/server/stopServer.js: -------------------------------------------------------------------------------- 1 | const { exec } = require("child_process") 2 | const kleur = require("kleur") 3 | const { platform } = require("os") 4 | const configure = require("../actions/configure") 5 | 6 | const stopServer = async (cfg) => { 7 | if (cfg.global.server_pid) { 8 | const command = 9 | platform() === "win32" 10 | ? `taskkill /PID ${cfg.global.server_pid} /F` 11 | : `kill ${cfg.global.server_pid}` 12 | 13 | return new Promise((resolve, reject) => { 14 | exec(command, (error) => { 15 | if (error) { 16 | reject(error) 17 | } else { 18 | resolve() 19 | } 20 | }) 21 | }) 22 | .then(() => { 23 | console.log(kleur.green("API server stopped!")) 24 | }) 25 | .catch(() => { 26 | console.log(kleur.yellow("API server is not running!")) 27 | }) 28 | .then(() => { 29 | configure("server_pid", "", cfg, true, true) 30 | }) 31 | } 32 | 33 | console.log(kleur.yellow("API server is not running!")) 34 | return false 35 | } 36 | 37 | module.exports = stopServer 38 | -------------------------------------------------------------------------------- /scripts/zipBinaries.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const { execSync } = require("child_process") 3 | const { version } = require("../package.json") 4 | 5 | const cmd = (name) => 6 | `zip -9 -j bin/gramma-${name}-v${version}.zip bin/${name}/gramma${ 7 | name.includes("windows") ? ".exe" : "" 8 | }` 9 | 10 | const main = () => { 11 | const folders = fs.readdirSync("bin").filter((name) => !name.includes(".zip")) 12 | 13 | folders.forEach((folder) => { 14 | console.log(`Creating zip file for ${folder}...`) 15 | execSync(cmd(folder)) 16 | console.log(`Zip file for ${folder} created!`) 17 | }) 18 | 19 | const versionRegex = /v\d\.\d\.\d/g 20 | 21 | const readme = fs 22 | .readFileSync("README.md") 23 | .toString() 24 | .replace(versionRegex, `v${version}`) 25 | 26 | fs.writeFileSync("README.md", readme) 27 | console.log("README links updated!") 28 | 29 | const website = fs 30 | .readFileSync("_layouts/default.html") 31 | .toString() 32 | .replace(versionRegex, `v${version}`) 33 | 34 | fs.writeFileSync("_layouts/default.html", website) 35 | console.log("Website links updated!") 36 | } 37 | 38 | main() 39 | -------------------------------------------------------------------------------- /src/utils/downloadFile.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const progressStream = require("progress-stream") 3 | const cliProgress = require("cli-progress") 4 | 5 | const toMegabytes = (bytes) => { 6 | return Number((bytes / (1000 * 1000)).toFixed(2)) 7 | } 8 | 9 | const downloadFile = async (url, path) => { 10 | const res = await fetch(url) 11 | const dataLength = res.headers.get("content-length") 12 | const bar = new cliProgress.Bar({ 13 | barCompleteChar: "#", 14 | barIncompleteChar: ".", 15 | format: "Downloading: [{bar}] {percentage}% | {value}/{total}MB", 16 | }) 17 | bar.start(toMegabytes(dataLength), 0) 18 | 19 | const str = progressStream({ 20 | length: dataLength, 21 | time: 100, 22 | }).on("progress", (progress) => bar.update(toMegabytes(progress.transferred))) 23 | 24 | const fileStream = fs.createWriteStream(path) 25 | 26 | return new Promise((resolve, reject) => { 27 | res.body.pipe(str).pipe(fileStream) 28 | res.body.on("error", (err) => { 29 | reject(err) 30 | }) 31 | fileStream.on("finish", () => { 32 | bar.stop() 33 | resolve() 34 | }) 35 | }) 36 | } 37 | 38 | module.exports = downloadFile 39 | -------------------------------------------------------------------------------- /src/prompts/handleSave.js: -------------------------------------------------------------------------------- 1 | const prompts = require("prompts") 2 | const { platform } = require("os") 3 | 4 | const initialFileName = (originalFile) => { 5 | const date = 6 | platform() === "win32" 7 | ? new Date().toISOString().replace(/[.:-]/g, "") 8 | : new Date().toISOString() 9 | 10 | return originalFile ? `${date}-${originalFile}` : `${date}-gramma.txt` 11 | } 12 | 13 | const handleSave = (mode, originalFile = null) => { 14 | const choices = [ 15 | ...(mode === "FILE" ? [{ title: "replace file", value: "replace" }] : []), 16 | { title: "save as", value: "save-as" }, 17 | { title: "print on screen", value: "print" }, 18 | ] 19 | 20 | const initialInput = mode === "FILE" ? 0 : 1 21 | 22 | return prompts([ 23 | { 24 | type: "select", 25 | name: "saveOption", 26 | message: "What do you want to do?", 27 | initial: initialInput, 28 | choices, 29 | }, 30 | { 31 | type: (prev) => (prev === "save-as" ? "text" : null), 32 | name: "fileName", 33 | initial: initialFileName(originalFile), 34 | message: "Please provide a file path", 35 | }, 36 | ]) 37 | } 38 | 39 | module.exports = handleSave 40 | -------------------------------------------------------------------------------- /src/actions/save.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const fs = require("fs") 3 | const kleur = require("kleur") 4 | const { homedir } = require("os") 5 | const handleSave = require("../prompts/handleSave") 6 | 7 | const save = async (text, mode, filePath = null) => { 8 | const originalFile = filePath ? path.basename(filePath) : null 9 | 10 | console.clear() 11 | console.log("All mistakes fixed!") 12 | const { saveOption, fileName } = await handleSave(mode, originalFile) 13 | 14 | if (saveOption === "replace") { 15 | fs.writeFileSync(filePath, text) 16 | console.clear() 17 | console.log(kleur.green("Saved!")) 18 | } else if (saveOption === "save-as") { 19 | const resolvedFileName = fileName.replace("~", homedir()) 20 | const newPath = path.resolve(process.cwd(), resolvedFileName) 21 | 22 | fs.writeFileSync(newPath, text) 23 | 24 | console.clear() 25 | console.log(kleur.green(`Saved as ${newPath}`)) 26 | } else { 27 | console.clear() 28 | console.log( 29 | `---------------------------------\n\n${text}\n\n---------------------------------\n${kleur.green( 30 | "Done!", 31 | )}`, 32 | ) 33 | } 34 | } 35 | 36 | module.exports = save 37 | -------------------------------------------------------------------------------- /src/actions/checkNonInteractively.js: -------------------------------------------------------------------------------- 1 | const kleur = require("kleur") 2 | const checkWithFallback = require("../requests/checkWithFallback") 3 | const Mistake = require("../components/Mistake") 4 | const { displayUpdates } = require("../requests/updates") 5 | 6 | const print = (result, styles) => { 7 | if (result.matches.length === 0) { 8 | console.log(kleur.green("No mistakes found!")) 9 | } else { 10 | console.log( 11 | `Found ${result.matches.length} potential mistake${ 12 | result.matches.length === 1 ? "" : "s" 13 | }`, 14 | ) 15 | console.log() 16 | console.log( 17 | result.matches.map((match) => Mistake(match, styles)).join("\n"), 18 | ) 19 | } 20 | } 21 | 22 | const checkNonInteractively = async (text, cfg, styles = true) => { 23 | if (!text || text.trim().length === 0) { 24 | console.log(kleur.yellow("Nothing to check!")) 25 | return 0 26 | } 27 | 28 | const result = await checkWithFallback(text, cfg) 29 | console.log(`Language: ${result.language.name}`) 30 | 31 | print(result, styles) 32 | 33 | await displayUpdates(cfg.paths.globalConfigDir) 34 | 35 | return result.matches.length === 0 ? 0 : 1 36 | } 37 | 38 | module.exports = checkNonInteractively 39 | -------------------------------------------------------------------------------- /src/requests/checkViaAPI.d.ts: -------------------------------------------------------------------------------- 1 | export = checkViaAPI 2 | /** 3 | * Calls the provided LanguageTool API 4 | * and returns grammar checker suggestions. 5 | * 6 | * @param text text to check 7 | * @param options request config 8 | * 9 | * @returns grammar checker suggestions 10 | */ 11 | declare function checkViaAPI( 12 | text: any, 13 | options?: { 14 | api_url?: string 15 | api_key?: string 16 | language?: string 17 | rules?: { [ruleName: string]: boolean } 18 | dictionary?: string[] 19 | markdown?: boolean 20 | }, 21 | ): Promise<{ 22 | language: { 23 | name: string 24 | code: string 25 | [key: string]: any 26 | } 27 | matches: { 28 | message: string 29 | shortMessage: string 30 | replacements: { value: string; [key: string]: any }[] 31 | offset: number 32 | length: number 33 | context: { text: string; offset: number; length: number } 34 | sentence: string 35 | type: { typeName: string } 36 | rule: { 37 | id: string 38 | description: string 39 | issueType: string 40 | category: { id: string; name: string } 41 | isPremium: false 42 | } 43 | word: string 44 | [key: string]: any 45 | }[] 46 | [key: string]: any 47 | }> 48 | -------------------------------------------------------------------------------- /src/components/FixMenu.js: -------------------------------------------------------------------------------- 1 | const kleur = require("kleur") 2 | 3 | const FixOptions = (fixes) => { 4 | if (fixes.length === 0) { 5 | return "" 6 | } 7 | if (fixes.length === 1) { 8 | return kleur.bold().green("1") + kleur.reset(`: fix\n`) 9 | } 10 | return kleur.bold().green(`1-${fixes.length}`) + kleur.reset(`: choose fix\n`) 11 | } 12 | 13 | const FixMenu = (fixes, issue) => { 14 | const defaultFix = kleur.bold().green(fixes.length > 0 ? 1 : 0) 15 | 16 | // prettier-ignore 17 | const dictionaryOptions = 18 | issue === "misspelling" 19 | ? `${kleur.bold().green("l") 20 | }${kleur.reset(`: add to local dictionary\n`) 21 | }${kleur.bold().green("g") 22 | }${kleur.reset(`: add to global dictionary\n`)}` 23 | : "" 24 | 25 | // prettier-ignore 26 | return ( 27 | `What do you want to do?\n${ 28 | kleur.bold().green("Enter") 29 | }${kleur.reset(`: default (${defaultFix})\n`) 30 | }${FixOptions(fixes) 31 | }${kleur.bold().green("0") 32 | }${kleur.reset(`: custom fix\n`) 33 | }${kleur.bold().green("i") 34 | }${kleur.reset(`: ignore\n`) 35 | }${dictionaryOptions 36 | }${kleur.bold().green("n") 37 | }${kleur.reset(`: next\n`)}` 38 | ) 39 | } 40 | 41 | module.exports = FixMenu 42 | -------------------------------------------------------------------------------- /src/commands/check.js: -------------------------------------------------------------------------------- 1 | const intercept = require("intercept-stdout") 2 | const kleur = require("kleur") 3 | const fs = require("fs") 4 | const checkNonInteractively = require("../actions/checkNonInteractively") 5 | const checkInteractively = require("../actions/checkInteractively") 6 | const save = require("../actions/save") 7 | const stripStyles = require("../utils/stripStyles") 8 | 9 | const check = async (argv, cfg) => { 10 | if (!argv.file) { 11 | console.log(kleur.red("Please provide a file path.")) 12 | process.exit(1) 13 | } 14 | 15 | if (!fs.existsSync(argv.file) || argv.file === "." || argv.file === "..") { 16 | console.log(kleur.red("There is no such file!")) 17 | process.exit(1) 18 | } 19 | 20 | const initialText = fs.readFileSync(argv.file).toString() 21 | 22 | if (argv.print) { 23 | const noColors = argv["no-colors"] 24 | 25 | if (noColors) { 26 | intercept(stripStyles) 27 | } 28 | 29 | const status = await checkNonInteractively(initialText, cfg, !noColors) 30 | process.exit(status) 31 | } else { 32 | const { changed, text } = await checkInteractively(initialText, cfg) 33 | if (changed) { 34 | await save(text, "FILE", argv.file) 35 | } 36 | process.exit() 37 | } 38 | } 39 | 40 | module.exports = check 41 | -------------------------------------------------------------------------------- /src/commands/server.js: -------------------------------------------------------------------------------- 1 | const kleur = require("kleur") 2 | const installServer = require("../server/installServer") 3 | const startServer = require("../server/startServer") 4 | const stopServer = require("../server/stopServer") 5 | const getServerPID = require("../server/getServerPID") 6 | const getServerInfo = require("../server/getServerInfo") 7 | const showServerGUI = require("../server/showServerGUI") 8 | 9 | const server = async (argv, cfg) => { 10 | const availableOptions = ["install", "start", "stop", "pid", "info", "gui"] 11 | 12 | if (!availableOptions.includes(argv.action)) { 13 | console.log(kleur.red("There is no such command!")) 14 | console.log( 15 | `Available options for gramma server: ${availableOptions.join(" | ")}`, 16 | ) 17 | process.exit(1) 18 | } 19 | 20 | if (argv.action === "install") { 21 | await installServer(cfg) 22 | process.exit() 23 | } 24 | 25 | if (argv.action === "start") { 26 | await startServer(cfg, { 27 | port: argv.port, 28 | viaCommand: true, 29 | }) 30 | process.exit() 31 | } 32 | 33 | if (argv.action === "stop") { 34 | await stopServer(cfg) 35 | process.exit() 36 | } 37 | 38 | if (argv.action === "pid") { 39 | getServerPID(cfg) 40 | process.exit() 41 | } 42 | 43 | if (argv.action === "info") { 44 | getServerInfo(cfg) 45 | process.exit() 46 | } 47 | 48 | if (argv.action === "gui") { 49 | showServerGUI(cfg) 50 | process.exit() 51 | } 52 | } 53 | 54 | module.exports = server 55 | -------------------------------------------------------------------------------- /src/server/installServer.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const fs = require("fs") 3 | const kleur = require("kleur") 4 | const rimraf = require("rimraf") 5 | const downloadFile = require("../utils/downloadFile") 6 | const unzipFile = require("../utils/unzipFile") 7 | const configure = require("../actions/configure") 8 | const confirmServerReinstall = require("../prompts/confirmServerReinstall") 9 | 10 | const installServer = async (cfg) => { 11 | const serverDir = path.join(cfg.paths.home, ".languagetool") 12 | const zipPath = path.join(serverDir, "languagetool.zip") 13 | 14 | if (fs.existsSync(serverDir)) { 15 | const { reinstall } = await confirmServerReinstall() 16 | 17 | if (reinstall) { 18 | rimraf.sync(serverDir) 19 | } else { 20 | console.log("Aborting!") 21 | process.exit() 22 | } 23 | } 24 | 25 | fs.mkdirSync(serverDir) 26 | 27 | await downloadFile( 28 | "https://languagetool.org/download/LanguageTool-stable.zip", 29 | zipPath, 30 | ) 31 | 32 | console.log("Unpacking...") 33 | 34 | await unzipFile(zipPath, serverDir) 35 | 36 | rimraf.sync(zipPath) 37 | 38 | console.log("Configuring...") 39 | 40 | const [unpackedDirName] = fs.readdirSync(serverDir) 41 | const serverPath = path.join(serverDir, unpackedDirName) 42 | 43 | configure("server_path", serverPath, cfg, true, true) 44 | configure("api_url", "localhost", cfg, true, true) 45 | 46 | console.log(kleur.green(`Server installed in: ${serverDir}`)) 47 | } 48 | 49 | module.exports = installServer 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.0.0 4 | 5 | First stable release. 6 | 7 | ## 1.1.0 8 | 9 | - Added Git hook integration 10 | - Updated dependencies and documentation 11 | - Improved error handling 12 | 13 | ## 1.2.0 14 | 15 | - Added Markdown support 16 | - Used api.languagetool.org as the default API 17 | 18 | ## 1.3.0 19 | 20 | - Support for environment variables in config files 21 | - Local config works in subdirectories 22 | - Automatic markdown support for .md files 23 | - Better error handling 24 | - Improved documentation 25 | 26 | ## 1.4.0 27 | 28 | - Automatically include changes to .gramma.json when executing Git hook 29 | - Standalone binaries migrated to Node 16 30 | 31 | ## 1.4.1 32 | 33 | - Fixed JS API, added type definitions 34 | - Fixed hooks behavior with commit --verbose flag 35 | 36 | ## 1.4.2 - 1.4.4 37 | 38 | - Isomorphic JS API (works on browser) 39 | 40 | ## 1.4.5 41 | 42 | - Fixed CORS in JS API (browser) 43 | 44 | ## 1.4.6 - 1.4.7 45 | 46 | - Bundles (esm, esm-min, iife) 47 | 48 | ## 1.4.8 49 | 50 | - Fixed links in README 51 | 52 | ## 1.5.0 53 | 54 | - When local server is installed but not running, Gramma will now try to use command-line interface for LanguageTool communication instead of spawning HTTP server (if possible). 55 | - Gramma will now automatically check for updates once a day. 56 | - Added validation for languages and rules parameters. 57 | 58 | ## 1.6.0 59 | 60 | - Added `gramma server info` command. 61 | - Added option to set custom port when managing local server manually. 62 | -------------------------------------------------------------------------------- /src/components/Mistake.js: -------------------------------------------------------------------------------- 1 | const kleur = require("kleur") 2 | const replace = require("../text-manipulation/replace") 3 | 4 | const getMistakeColor = (type) => { 5 | if (type === "grammar") { 6 | return "red" 7 | } 8 | 9 | if (type === "style") { 10 | return "blue" 11 | } 12 | 13 | return "yellow" 14 | } 15 | 16 | const highlightMistake = (context, type, offset, length) => { 17 | const color = getMistakeColor(type) 18 | const change = (mistake) => kleur[color](mistake) 19 | 20 | return replace(context, change, offset, length) 21 | } 22 | 23 | const Mistake = (match, style = true) => { 24 | const context = highlightMistake( 25 | match.context.text, 26 | match.rule.issueType, 27 | match.context.offset, 28 | match.context.length, 29 | ) 30 | 31 | const replacements = match.replacements 32 | .map( 33 | (replacement, index) => 34 | `${kleur.bold().green(index + 1)}) ${replacement.value}`, 35 | ) 36 | .join(" ") 37 | 38 | const fixes = 39 | match.replacements.length > 0 40 | ? `${kleur.bold("Suggested fix:")} ${replacements}\n` 41 | : "" 42 | 43 | const word = style ? "" : `Word: ${match.word}\n` 44 | 45 | // prettier-ignore 46 | return ( 47 | `---------------------------------\n\n${ 48 | kleur.dim(`${kleur.bold("Rule:")} ${match.rule.category.id.toLowerCase()}\n`) 49 | }${kleur.dim(`${kleur.bold("Explanation:")} ${match.message}\n\n`) 50 | }${word 51 | }${kleur.bold("Context:")} ${context}\n${ 52 | fixes}` 53 | ) 54 | } 55 | 56 | module.exports = Mistake 57 | -------------------------------------------------------------------------------- /data/rules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "CASING", 4 | "description": "Detecting uppercase words where lowercase is required and vice versa." 5 | }, 6 | { 7 | "id": "COLLOQUIALISMS", 8 | "description": "Colloquial style." 9 | }, 10 | { 11 | "id": "COMPOUNDING", 12 | "description": "Rules about spelling terms as one word or as as separate words." 13 | }, 14 | { 15 | "id": "CONFUSED_WORDS", 16 | "description": "Words that are easily confused, like 'there' and 'their' in English." 17 | }, 18 | { 19 | "id": "FALSE_FRIENDS", 20 | "description": "Words easily confused by language learners because a similar word exists in their native language." 21 | }, 22 | { 23 | "id": "GENDER_NEUTRALITY", 24 | "description": "" 25 | }, 26 | { 27 | "id": "GRAMMAR", 28 | "description": "" 29 | }, 30 | { 31 | "id": "MISC", 32 | "description": "Miscellaneous rules that don't fit elsewhere." 33 | }, 34 | { 35 | "id": "PUNCTUATION", 36 | "description": "" 37 | }, 38 | { 39 | "id": "REDUNDANCY", 40 | "description": "" 41 | }, 42 | { 43 | "id": "REGIONALISMS", 44 | "description": "Words used only in another language variant or used with different meanings." 45 | }, 46 | { 47 | "id": "REPETITIONS", 48 | "description": "" 49 | }, 50 | { 51 | "id": "SEMANTICS", 52 | "description": "Logic, content, and consistency problems." 53 | }, 54 | { 55 | "id": "STYLE", 56 | "description": "General style issues not covered by other categories, like overly verbose wording." 57 | }, 58 | { 59 | "id": "TYPOGRAPHY", 60 | "description": "Problems like incorrectly used dash or quote characters." 61 | }, 62 | { 63 | "id": "TYPOS", 64 | "description": "Spelling issues." 65 | } 66 | ] 67 | -------------------------------------------------------------------------------- /src/components/FixMenu.test.js: -------------------------------------------------------------------------------- 1 | const stripStyles = require("../utils/stripStyles") 2 | const FixMenu = require("./FixMenu") 3 | 4 | describe("FixMenu component", () => { 5 | it("renders menu with multiple fix propositions for mistake", () => { 6 | const expected = 7 | "What do you want to do?\n" + 8 | "Enter: default (1)\n" + 9 | "1-3: choose fix\n" + 10 | "0: custom fix\n" + 11 | "i: ignore\n" + 12 | "n: next\n" 13 | 14 | const result = FixMenu([{}, {}, {}]) 15 | 16 | const rawResult = stripStyles(result) 17 | 18 | expect(rawResult).toEqual(expected) 19 | }) 20 | 21 | it("renders menu with single fix proposition for mistake", () => { 22 | const expected = 23 | "What do you want to do?\n" + 24 | "Enter: default (1)\n" + 25 | "1: fix\n" + 26 | "0: custom fix\n" + 27 | "i: ignore\n" + 28 | "n: next\n" 29 | 30 | const result = FixMenu([{}]) 31 | 32 | const rawResult = stripStyles(result) 33 | 34 | expect(rawResult).toEqual(expected) 35 | }) 36 | 37 | it("renders menu with no fix propositions for mistake", () => { 38 | const expected = 39 | "What do you want to do?\n" + 40 | "Enter: default (0)\n" + 41 | "0: custom fix\n" + 42 | "i: ignore\n" + 43 | "n: next\n" 44 | 45 | const result = FixMenu([]) 46 | 47 | const rawResult = stripStyles(result) 48 | 49 | expect(rawResult).toEqual(expected) 50 | }) 51 | 52 | it("renders dictionary options on spelling mistake", () => { 53 | const expected = 54 | "What do you want to do?\n" + 55 | "Enter: default (0)\n" + 56 | "0: custom fix\n" + 57 | "i: ignore\n" + 58 | "l: add to local dictionary\n" + 59 | "g: add to global dictionary\n" + 60 | "n: next\n" 61 | 62 | const result = FixMenu([], "misspelling") 63 | 64 | const rawResult = stripStyles(result) 65 | 66 | expect(rawResult).toEqual(expected) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /src/requests/updates.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const path = require("path") 3 | const kleur = require("kleur") 4 | const { version } = require("../../package.json") 5 | 6 | const checkForUpdates = async (configDir) => { 7 | const updateFile = path.join(configDir, ".update") 8 | 9 | if (fs.existsSync(updateFile)) { 10 | const lastCheck = Number(fs.readFileSync(updateFile).toString()) 11 | const fullDay = 24 * 60 * 60 * 1000 12 | 13 | if (Date.now() - lastCheck < fullDay) { 14 | return { available: false } 15 | } 16 | } 17 | 18 | const timeout = () => new Promise((_, reject) => setTimeout(reject, 1000)) 19 | 20 | try { 21 | const response = await Promise.race([ 22 | fetch("https://api.github.com/repos/caderek/gramma/releases/latest"), 23 | timeout(), 24 | ]) 25 | fs.writeFileSync(updateFile, String(Date.now())) 26 | 27 | const data = await response.json() 28 | 29 | const remoteVersion = data.tag_name 30 | const [remoteMajor, remoteMinor, remotePatch] = remoteVersion 31 | .slice(1) 32 | .split(".") 33 | .map(Number) 34 | const [major, minor, patch] = version.split(".").map(Number) 35 | 36 | const oldVersion = major * 1e8 + minor * 1e5 + patch * 1e2 37 | const newVersion = remoteMajor * 1e8 + remoteMinor * 1e5 + remotePatch * 1e2 38 | 39 | if (newVersion > oldVersion) { 40 | return { available: true, newVersion: remoteVersion } 41 | } 42 | 43 | return { available: false } 44 | } catch (e) { 45 | return { available: false } 46 | } 47 | } 48 | 49 | const displayUpdates = async (configDir) => { 50 | const { available, newVersion } = await checkForUpdates(configDir) 51 | 52 | if (available) { 53 | console.log( 54 | kleur.yellow(` 55 | Update available: ${newVersion} 56 | Install via NPM or download the new binary from: 57 | https://caderek.github.io/gramma/ 58 | `), 59 | ) 60 | } 61 | } 62 | 63 | exports.checkForUpdates = checkForUpdates 64 | exports.displayUpdates = displayUpdates 65 | -------------------------------------------------------------------------------- /src/requests/checkWithFallback.js: -------------------------------------------------------------------------------- 1 | const kleur = require("kleur") 2 | const startServer = require("../server/startServer") 3 | const checkViaAPI = require("./checkViaAPI") 4 | const checkViaCmd = require("./checkViaCmd") 5 | const stopServer = require("../server/stopServer") 6 | 7 | const checkWithFallback = async (text, cfg) => { 8 | const { session, global } = cfg 9 | let response 10 | 11 | try { 12 | console.info(`Checking via ${cfg.session.api_url}...`) 13 | 14 | response = await checkViaAPI(text, session) 15 | 16 | if ( 17 | cfg.session.api_url.includes("localhost") && 18 | cfg.session.server_once === "true" 19 | ) { 20 | await stopServer(cfg) 21 | } 22 | } catch (error) { 23 | if (error.code === "ECONNREFUSED" || cfg.session.api_url === "localhost") { 24 | if (global.server_path) { 25 | if (!session.markdown) { 26 | console.info(`Checking via local LanguageTool cmd...`) 27 | 28 | response = await checkViaCmd( 29 | text, 30 | session, 31 | global.server_path, 32 | cfg.paths.globalConfigDir, 33 | ) 34 | } else { 35 | const { server, api_url } = await startServer(cfg) 36 | console.clear() 37 | const updatedSession = { ...session, api_url } 38 | response = await checkViaAPI(text, updatedSession) 39 | 40 | if (global.server_once === "true") { 41 | server.kill() 42 | } 43 | } 44 | } else { 45 | console.log(kleur.red(`API server ${session.api_url} not available!`)) 46 | console.log("Please make sure that the server is running.") 47 | console.log( 48 | "TIP: Gramma is able to automatically start local API server if you install it via: gramma server install", 49 | ) 50 | process.exit(1) 51 | } 52 | } else { 53 | console.log("Gramma was unable to get a response from API server.") 54 | console.log(`Details: ${error.message}`) 55 | process.exit(1) 56 | } 57 | } 58 | 59 | return response 60 | } 61 | 62 | module.exports = checkWithFallback 63 | -------------------------------------------------------------------------------- /.gramma.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_url": "https://api.languagetool.org/v2/check", 3 | "api_key": "", 4 | "dictionary": [ 5 | "Asturian", 6 | "Bugfix", 7 | "CHANGELOG", 8 | "CircleCI", 9 | "Codacy", 10 | "CommonJS", 11 | "Config", 12 | "Github", 13 | "Gramma", 14 | "Grammarbot", 15 | "IIFE", 16 | "JS", 17 | "Moçambique", 18 | "NPM", 19 | "README", 20 | "XXXXXXXX", 21 | "YYYYYYYY", 22 | "_blank", 23 | "api", 24 | "api_key", 25 | "ast-ES", 26 | "async", 27 | "backend", 28 | "boolean", 29 | "br-FR", 30 | "chmod", 31 | "config", 32 | "confused_words", 33 | "const", 34 | "correctText", 35 | "da-DK", 36 | "de", 37 | "de-AT", 38 | "de-CH", 39 | "de-DE", 40 | "dev", 41 | "el-GR", 42 | "eo", 43 | "eslintignore", 44 | "esm", 45 | "esm-min", 46 | "exampleReplacements", 47 | "false_friends", 48 | "foo", 49 | "fr", 50 | "gender_neutrality", 51 | "gl-ES", 52 | "gramma", 53 | "grammarbot", 54 | "gui", 55 | "href", 56 | "iife", 57 | "img", 58 | "init", 59 | "io", 60 | "ja-JP", 61 | "js", 62 | "json", 63 | "km-KH", 64 | "linter", 65 | "nl", 66 | "npm", 67 | "npmignore", 68 | "pid", 69 | "preAO", 70 | "prepareReplacements", 71 | "replaceAll", 72 | "rimraf", 73 | "ro-RO", 74 | "ru-RU", 75 | "signup", 76 | "sk-SK", 77 | "sl-SI", 78 | "src", 79 | "stdin", 80 | "stdout", 81 | "stylesheet", 82 | "sv", 83 | "symlink", 84 | "tl-PH", 85 | "uk-UA", 86 | "url", 87 | "usedCfg", 88 | "zh-CN" 89 | ], 90 | "language": "en-US", 91 | "rules": { 92 | "casing": true, 93 | "colloquialisms": true, 94 | "compounding": true, 95 | "confused_words": true, 96 | "false_friends": true, 97 | "gender_neutrality": true, 98 | "grammar": true, 99 | "misc": true, 100 | "punctuation": true, 101 | "redundancy": true, 102 | "regionalisms": true, 103 | "repetitions": true, 104 | "semantics": true, 105 | "style": true, 106 | "typography": false, 107 | "typos": true 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "{{ site.theme }}"; 5 | 6 | body { 7 | margin: 0; 8 | } 9 | 10 | .page-header { 11 | color: #fff; 12 | text-align: center; 13 | background-color: #0081b8; 14 | background-image: linear-gradient(120deg, #0081b8, #b8e045); 15 | padding: 15px; 16 | } 17 | 18 | .main-content h1, 19 | .main-content h4, 20 | .main-content h5, 21 | .main-content h6 { 22 | color: #0f9250; 23 | } 24 | 25 | .main-content h2 { 26 | color: white; 27 | background: linear-gradient(to right, #0081b8, #b8e045); 28 | padding: 5px 10px; 29 | margin-top: 50px; 30 | } 31 | 32 | .main-content h3 { 33 | color: black; 34 | background: linear-gradient(to right, #b5ddee, #e8f3c7); 35 | padding: 5px 10px; 36 | margin-top: 30px; 37 | } 38 | 39 | .project-tagline { 40 | font-family: monospace; 41 | } 42 | 43 | .download { 44 | display: inline-block; 45 | padding: 10px; 46 | opacity: 0.8; 47 | width: 200px; 48 | text-align: center; 49 | margin-top: 10px; 50 | cursor: pointer; 51 | transition-duration: 0.2s; 52 | } 53 | 54 | .download:hover { 55 | opacity: 1; 56 | } 57 | 58 | .download i { 59 | font-size: 50px; 60 | margin: 10px; 61 | } 62 | 63 | .download__link, 64 | .download__link:visited { 65 | text-decoration: none; 66 | color: white; 67 | outline: none; 68 | } 69 | 70 | .download__link:hover { 71 | text-decoration: none; 72 | color: white; 73 | } 74 | 75 | @media only screen and (max-width: 640px) { 76 | .download { 77 | display: block; 78 | width: auto; 79 | height: auto; 80 | text-align: left; 81 | margin: 0; 82 | } 83 | 84 | .download i { 85 | font-size: 30px; 86 | margin: 0 10px; 87 | } 88 | 89 | p { 90 | display: initial; 91 | position: relative; 92 | top: -8px; 93 | } 94 | } 95 | 96 | .divider { 97 | display: none; 98 | } 99 | 100 | .version { 101 | color: white; 102 | position: absolute; 103 | top: 10px; 104 | right: 15px; 105 | font-size: 20px; 106 | opacity: 0.8; 107 | } 108 | 109 | .actions { 110 | text-align: center; 111 | padding: 0 10px 30px 10px; 112 | } 113 | 114 | .actions--bottom { 115 | padding: 30px 10px 0 10px; 116 | } 117 | 118 | .site-footer { 119 | text-align: center; 120 | } 121 | -------------------------------------------------------------------------------- /scripts/checkLanguagesSupport.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const querystring = require("querystring") 3 | const fetch = require("node-fetch") 4 | 5 | const LOCAL_API_URL = "http://localhost:8082/v2/languages" 6 | 7 | const checkSupport = async (language, api) => { 8 | const postData = querystring.stringify({ 9 | api_key: "", 10 | language, 11 | text: "abc", 12 | // ...disabledRulesEntry, 13 | }) 14 | 15 | const response = await fetch(api, { 16 | credentials: "include", 17 | headers: { 18 | "Content-Type": "application/x-www-form-urlencoded", 19 | }, 20 | body: postData, 21 | method: "POST", 22 | }) 23 | 24 | console.log(`Checking ${language} at ${api}`) 25 | 26 | try { 27 | await response.json() 28 | } catch (e) { 29 | return false 30 | } 31 | return true 32 | } 33 | 34 | const LANGUAGETOOL_ORG_LIMIT = 20 // req/min 35 | const interval = (60 * 1000) / LANGUAGETOOL_ORG_LIMIT 36 | const delay = () => new Promise((resolve) => setTimeout(resolve, interval)) 37 | 38 | const main = async () => { 39 | const res = await fetch(LOCAL_API_URL) 40 | const languages = await res.json() 41 | 42 | for (const language of languages) { 43 | language.grammarbotIo = await checkSupport( 44 | language.longCode, 45 | "http://api.grammarbot.io/v2/check", 46 | ) 47 | language.languagetoolOrg = await checkSupport( 48 | language.longCode, 49 | "https://api.languagetool.org/v2/check", 50 | ) 51 | 52 | await delay() 53 | } 54 | 55 | fs.writeFileSync("data/languages.json", JSON.stringify(languages, null, 2)) 56 | 57 | const entries = languages 58 | .map( 59 | ({ name, longCode, grammarbotIo, languagetoolOrg }) => 60 | ` 61 | 62 | ${longCode} 63 | ${name} 64 | ${languagetoolOrg ? "✔" : "-"} 65 | ${grammarbotIo ? "✔" : "-"} 66 | ✔ 67 | 68 | `, 69 | ) 70 | .join("") 71 | 72 | const docs = `\n${entries}\n `.replace( 73 | /^\s*[\r\n]/gm, 74 | "", 75 | ) 76 | 77 | const readme = fs 78 | .readFileSync("README.md") 79 | .toString() 80 | .replace(/(.|\n)+/, docs) 81 | 82 | fs.writeFileSync("README.md", readme) 83 | console.log("README entries updated!") 84 | } 85 | 86 | main() 87 | -------------------------------------------------------------------------------- /src/components/Mistake.test.js: -------------------------------------------------------------------------------- 1 | const stripStyles = require("../utils/stripStyles") 2 | const Mistake = require("./Mistake") 3 | 4 | describe("Mistake component", () => { 5 | it("renders info about mistake without suggestions", () => { 6 | const expected = 7 | `---------------------------------\n\n` + 8 | `Rule: typos\n` + 9 | `Explanation: Did you mean "is"?\n\n` + 10 | `Context: It are a perfect English sentence. \n` 11 | 12 | const result = Mistake({ 13 | message: 'Did you mean "is"?', 14 | replacements: [], 15 | context: { 16 | text: " It are a perfect English sentence. ", 17 | offset: 4, 18 | length: 3, 19 | }, 20 | rule: { 21 | category: { 22 | id: "typos", 23 | }, 24 | }, 25 | }) 26 | 27 | const rawResult = stripStyles(result) 28 | 29 | expect(rawResult).toEqual(expected) 30 | }) 31 | 32 | it("renders info about mistake with single suggestion", () => { 33 | const expected = 34 | `---------------------------------\n\n` + 35 | `Rule: typos\n` + 36 | `Explanation: Some message\n\n` + 37 | `Context: Some context\n` + 38 | `Suggested fix: 1) foo\n` 39 | 40 | const result = Mistake({ 41 | message: "Some message", 42 | replacements: [{ value: "foo" }], 43 | context: { 44 | text: "Some context", 45 | offset: 4, 46 | length: 3, 47 | }, 48 | rule: { 49 | category: { 50 | id: "typos", 51 | }, 52 | }, 53 | }) 54 | 55 | const rawResult = stripStyles(result) 56 | 57 | expect(rawResult).toEqual(expected) 58 | }) 59 | 60 | it("renders info about mistake with multiple suggestions", () => { 61 | const expected = 62 | `---------------------------------\n\n` + 63 | `Rule: typos\n` + 64 | `Explanation: Some message\n\n` + 65 | `Context: Some context\n` + 66 | `Suggested fix: 1) foo 2) bar 3) baz\n` 67 | 68 | const result = Mistake({ 69 | message: "Some message", 70 | replacements: [{ value: "foo" }, { value: "bar" }, { value: "baz" }], 71 | context: { 72 | text: "Some context", 73 | offset: 4, 74 | length: 3, 75 | }, 76 | rule: { 77 | category: { 78 | id: "typos", 79 | }, 80 | }, 81 | }) 82 | 83 | const rawResult = stripStyles(result) 84 | 85 | expect(rawResult).toEqual(expected) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /src/server/startServer.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require("child_process") 2 | const path = require("path") 3 | const kleur = require("kleur") 4 | const portfinder = require("portfinder") 5 | const tcpPortUsed = require("tcp-port-used") 6 | const configure = require("../actions/configure") 7 | const confirmPort = require("../prompts/confirmPort") 8 | 9 | const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) 10 | 11 | const pingServer = async (url) => { 12 | console.log("Waiting for local API server...") 13 | const response = await fetch(`${url}?language=en-US&text=`).catch(() => { 14 | return { 15 | status: 500, 16 | } 17 | }) 18 | 19 | if (response.status === 200) { 20 | return 21 | } 22 | 23 | await delay(1000) 24 | await pingServer(url) 25 | } 26 | 27 | const startServer = async (cfg, { port = null, viaCommand = false } = {}) => { 28 | if (!cfg.global.server_path) { 29 | console.log( 30 | kleur.red(`Please install local server via: gramma server install`), 31 | ) 32 | process.exit(1) 33 | } 34 | 35 | if (port !== null) { 36 | const inUse = tcpPortUsed.check(port) 37 | 38 | if (inUse) { 39 | const { autoPort } = await confirmPort() 40 | 41 | if (!autoPort) { 42 | console.log(kleur.yellow("Aborted!")) 43 | process.exit(1) 44 | } 45 | } 46 | } 47 | 48 | console.log("Starting local API server...") 49 | 50 | const PORT = await portfinder.getPortPromise({ 51 | port: port || 8081, 52 | }) 53 | 54 | const command = "java" 55 | 56 | const params = [ 57 | "-cp", 58 | path.join(cfg.global.server_path, "languagetool-server.jar"), 59 | "org.languagetool.server.HTTPServer", 60 | "--port", 61 | String(PORT), 62 | "--allow-origin", 63 | "'*'", 64 | ] 65 | 66 | const server = spawn(command, params, { windowsHide: true, detached: true }) 67 | 68 | server.on("error", (error) => { 69 | if (error) { 70 | console.log(kleur.red("Cannot start local API server automatically.")) 71 | process.exit(1) 72 | } 73 | }) 74 | 75 | // eslint-disable-next-line camelcase 76 | const api_url = `http://localhost:${PORT}/v2/check` 77 | 78 | await pingServer(api_url) 79 | 80 | configure("api_url", api_url, cfg, true, true) 81 | 82 | if (cfg.global.server_once !== "true" || viaCommand) { 83 | configure("server_pid", server.pid, cfg, true, true) 84 | } 85 | 86 | console.log( 87 | kleur.green(`API server started!\nPID: ${server.pid}\nAPI URL: ${api_url}`), 88 | ) 89 | 90 | return { server, api_url } 91 | } 92 | 93 | module.exports = startServer 94 | -------------------------------------------------------------------------------- /src/actions/configure.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const kleur = require("kleur") 3 | const { isRule, ruleOptions } = require("../validators/rules") 4 | const { isLanguage, languageOptions } = require("../validators/languages") 5 | 6 | const availableOptions = [ 7 | "api_key", 8 | "api_url", 9 | "dictionary", 10 | "server_once", 11 | "language", 12 | "enable", 13 | "disable", 14 | ] 15 | 16 | const addToDictionary = (dictionary, word) => { 17 | const dict = Array.isArray(dictionary) ? dictionary : [] 18 | 19 | if (dict.includes(word)) { 20 | return dict 21 | } 22 | 23 | return [...dict, word].sort() 24 | } 25 | 26 | const changeRule = (rules, ruleName, isEnabled) => { 27 | return { ...rules, [ruleName]: isEnabled } 28 | } 29 | 30 | const prepareEntry = (key, value, cfg) => { 31 | if (key === "dictionary") { 32 | return { dictionary: addToDictionary(cfg.dictionary, value) } 33 | } 34 | 35 | if (key === "enable" || key === "disable") { 36 | if (!isRule(value)) { 37 | console.log(kleur.red("There is no such rule")) 38 | console.log(`Available options: ${ruleOptions.join(", ")}`) 39 | process.exit(1) 40 | } 41 | 42 | return { rules: changeRule(cfg.rules, value, key === "enable") } 43 | } 44 | 45 | if (key === "language" && !isLanguage(value)) { 46 | console.log(kleur.red("There is no such language option")) 47 | console.log(`Available options: ${languageOptions.join(", ")}`) 48 | process.exit(1) 49 | } 50 | 51 | return { [key]: value } 52 | } 53 | 54 | const configure = (key, value, cfg, isGlobal = false, internal = false) => { 55 | if (!availableOptions.includes(key) && !internal) { 56 | console.log(kleur.red(`There is no '${key}' option!`)) 57 | console.log("Available options:") 58 | console.log(availableOptions.join("\n")) 59 | process.exit(1) 60 | } 61 | 62 | if (key === "server_once" && !isGlobal) { 63 | console.log( 64 | kleur.red("This setting can be used only with -g (--global) flag"), 65 | ) 66 | process.exit(1) 67 | } 68 | 69 | const currentConfig = isGlobal ? cfg.global : cfg.local 70 | 71 | const configFilePath = isGlobal 72 | ? cfg.paths.globalConfigFile 73 | : cfg.paths.localConfigFile 74 | 75 | const entry = prepareEntry(key, value, currentConfig) 76 | 77 | const updatedConfig = { ...currentConfig, ...entry } 78 | 79 | if (isGlobal) { 80 | // eslint-disable-next-line no-param-reassign 81 | cfg.global = updatedConfig 82 | } else { 83 | // eslint-disable-next-line no-param-reassign 84 | cfg.local = updatedConfig 85 | } 86 | 87 | fs.writeFileSync(configFilePath, JSON.stringify(updatedConfig, null, 2)) 88 | } 89 | 90 | module.exports = configure 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gramma", 3 | "version": "1.6.0", 4 | "license": "ISC", 5 | "repository": "https://github.com/caderek/gramma", 6 | "homepage": "https://caderek.github.io/gramma/", 7 | "description": "Command line grammar checker", 8 | "main": "src/index.js", 9 | "bin": "src/cli.js", 10 | "exports": { 11 | ".": "./src/index.js", 12 | "./esm": "./bundle/gramma.esm.js", 13 | "./esm-min": "./bundle/gramma.esm.min.js", 14 | "./iife": "./bundle/gramma.min.js" 15 | }, 16 | "types": "src/index.d.ts", 17 | "scripts": { 18 | "build": "rm -rf bin; yarn run build:win64; yarn run build:macos; yarn run build:linux64; yarn run build:zip; yarn run build:bundles", 19 | "build:win64": "pkg -c package.json -t node16-win-x64 --out-path bin/windows64 src/cli.js", 20 | "build:macos": "pkg -c package.json -t node16-macos-x64 --out-path bin/macos src/cli.js", 21 | "build:linux64": "pkg -c package.json -t node16-linux-x64 --out-path bin/linux64 src/cli.js", 22 | "build:bundles": "yarn run build:esm; yarn run build:esm-min; yarn run build:iife", 23 | "build:esm": "esbuild src/index.js --bundle --outfile=bundle/gramma.esm.js --format=esm", 24 | "build:esm-min": "esbuild src/index.js --bundle --outfile=bundle/gramma.esm.min.js --format=esm --minify", 25 | "build:iife": "esbuild src/index.js --bundle --outfile=bundle/gramma.min.js --format=iife --minify --global-name=gramma", 26 | "build:zip": "node scripts/zipBinaries.js", 27 | "format": "prettier --write \"src/**/*.js\"", 28 | "lint": "eslint src/**", 29 | "test": "jest", 30 | "test:ci": "jest --coverage && cat ./coverage/lcov.info | codacy-coverage", 31 | "check:langs": "node scripts/checkLanguagesSupport.js", 32 | "prepare": "husky install", 33 | "definitions": "tsc" 34 | }, 35 | "keywords": [ 36 | "grammar", 37 | "command-line", 38 | "checker" 39 | ], 40 | "author": "Maciej Cąderek | maciej.caderek@gmail.com", 41 | "dependencies": { 42 | "cli-progress": "^3.9.1", 43 | "decompress": "^4.2.1", 44 | "decompress-unzip": "^4.0.1", 45 | "dotenv": "^10.0.0", 46 | "intercept-stdout": "^0.1.2", 47 | "isomorphic-fetch": "^3.0.0", 48 | "kleur": "^4.1.4", 49 | "portfinder": "^1.0.28", 50 | "progress-stream": "^2.0.0", 51 | "prompts": "^2.4.1", 52 | "query-string": "^7.0.1", 53 | "rimraf": "^3.0.2", 54 | "tcp-port-used": "^1.0.2", 55 | "yargs": "^17.2.1" 56 | }, 57 | "devDependencies": { 58 | "@types/jest": "^27.0.2", 59 | "codacy-coverage": "^3.4.0", 60 | "esbuild": "^0.13.4", 61 | "eslint": "^7.32.0", 62 | "eslint-config-airbnb": "^18.2.1", 63 | "eslint-config-prettier": "^8.3.0", 64 | "eslint-plugin-import": "^2.24.2", 65 | "eslint-plugin-jsx-a11y": "^6.4.1", 66 | "eslint-plugin-react": "^7.26.1", 67 | "gramma": "^1.6.0", 68 | "husky": "^7.0.0", 69 | "jest": "^27.2.4", 70 | "pkg": "^5.3.3", 71 | "prettier": "^2.4.1", 72 | "shelljs": "^0.8.4", 73 | "typescript": "^4.4.3" 74 | }, 75 | "jest": { 76 | "verbose": true, 77 | "testMatch": [ 78 | "**/?(*.)(spec|test).?(m)js" 79 | ] 80 | }, 81 | "engines": { 82 | "node": ">=12.0.0" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/requests/checkViaAPI.js: -------------------------------------------------------------------------------- 1 | const queryString = require("query-string") 2 | const initialConfig = require("../initialConfig") 3 | // @ts-ignore 4 | const prepareMarkdown = require("../utils/prepareMarkdown").default 5 | 6 | const addWordFields = (matches) => { 7 | return matches.map((match) => { 8 | const word = match.context.text.substr( 9 | match.context.offset, 10 | match.context.length, 11 | ) 12 | 13 | return { ...match, word } 14 | }) 15 | } 16 | 17 | const removeFalsePositives = (matches, dictionary, disabledRules) => { 18 | return matches.filter( 19 | (match) => 20 | !disabledRules.includes(match.rule.category.id) && 21 | !( 22 | match.rule.issueType === "misspelling" && 23 | dictionary.includes(match.word) 24 | ), 25 | ) 26 | } 27 | 28 | const MAX_REPLACEMENTS = 30 29 | 30 | /** 31 | * Calls the provided LanguageTool API 32 | * and returns grammar checker suggestions. 33 | * 34 | * @param {string} text text to check 35 | * @param {Object} options request config 36 | * 37 | * @returns {Promise} grammar checker suggestions 38 | */ 39 | const checkViaAPI = async (text, options = {}) => { 40 | const cfg = { ...initialConfig, ...options } 41 | const disabledRules = Object.entries(cfg.rules) 42 | // eslint-disable-next-line no-unused-vars 43 | .filter(([rule, value]) => value === false) 44 | .map(([rule]) => rule.toUpperCase()) 45 | 46 | const disabledRulesEntry = 47 | disabledRules.length === 0 || cfg.api_url.includes("grammarbot") 48 | ? {} 49 | : { disabledCategories: disabledRules.join(",") } 50 | 51 | const input = options.markdown ? { data: prepareMarkdown(text) } : { text } 52 | 53 | const postData = queryString.stringify({ 54 | api_key: cfg.api_key, 55 | language: cfg.language, 56 | ...input, 57 | ...disabledRulesEntry, 58 | }) 59 | 60 | // eslint-disable-next-line 61 | const response = await fetch(cfg.api_url, { 62 | headers: { 63 | "Content-Type": "application/x-www-form-urlencoded", 64 | }, 65 | body: postData, 66 | method: "POST", 67 | }) 68 | 69 | const body = await response.text() 70 | 71 | let result 72 | 73 | try { 74 | result = JSON.parse(body) 75 | } catch (e) { 76 | if (cfg.api_url.includes("grammarbot")) { 77 | throw new Error( 78 | "Language not available at grammarbot.io.\n" + 79 | "Please consider installing a local LanguageTool server:\n" + 80 | "https://github.com/caderek/gramma#installing-local-server", 81 | ) 82 | } else { 83 | throw new Error(body) 84 | } 85 | } 86 | 87 | const resultWithWords = { 88 | ...result, 89 | matches: removeFalsePositives( 90 | addWordFields(result.matches), 91 | cfg.dictionary, 92 | cfg.api_url === initialConfig.api_url ? disabledRules : [], 93 | ), 94 | } 95 | 96 | resultWithWords.matches.forEach((match) => { 97 | if (match.replacements.length > MAX_REPLACEMENTS) { 98 | match.replacements.length = MAX_REPLACEMENTS // eslint-disable-line 99 | } 100 | }) 101 | 102 | return resultWithWords 103 | } 104 | 105 | module.exports = checkViaAPI 106 | -------------------------------------------------------------------------------- /src/requests/checkViaCmd.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const path = require("path") 3 | const kleur = require("kleur") 4 | const { execSync } = require("child_process") 5 | const initialConfig = require("../initialConfig") 6 | 7 | const addWordFields = (matches) => { 8 | return matches.map((match) => { 9 | const word = match.context.text.substr( 10 | match.context.offset, 11 | match.context.length, 12 | ) 13 | 14 | return { ...match, word } 15 | }) 16 | } 17 | 18 | const removeFalsePositives = (matches, dictionary, disabledRules) => { 19 | return matches.filter( 20 | (match) => 21 | !disabledRules.includes(match.rule.category.id) && 22 | !( 23 | match.rule.issueType === "misspelling" && 24 | dictionary.includes(match.word) 25 | ), 26 | ) 27 | } 28 | 29 | const createTempFile = (file, text) => { 30 | fs.writeFileSync(file, text) 31 | } 32 | 33 | const removeTempFile = (file) => { 34 | fs.unlinkSync(file) 35 | } 36 | 37 | const MAX_REPLACEMENTS = 30 38 | 39 | /** 40 | * Calls the provided LanguageTool API 41 | * and returns grammar checker suggestions. 42 | * 43 | * @param {string} text text to check 44 | * @param {Object} options request config 45 | * 46 | * @returns {Promise} grammar checker suggestions 47 | */ 48 | const checkViaCmd = async ( 49 | text, 50 | options = {}, 51 | serverDirPath, 52 | configDirPath, 53 | ) => { 54 | const cfg = { ...initialConfig, ...options } 55 | // console.log({ cfg, serverDirPath, configDirPath }) 56 | 57 | const disabledRules = Object.entries(cfg.rules) 58 | // eslint-disable-next-line no-unused-vars 59 | .filter(([rule, value]) => value === false) 60 | .map(([rule]) => rule.toUpperCase()) 61 | 62 | const tempFile = path.join(configDirPath, ".temp") 63 | 64 | createTempFile(tempFile, text) 65 | 66 | const jar = path.join(serverDirPath, "languagetool-commandline.jar") 67 | const lang = cfg.language === "auto" ? " -adl" : ` -l ${cfg.language}` 68 | const disabled = 69 | disabledRules.length === 0 ? "" : ` -d ${disabledRules.join(",")}` 70 | 71 | const cmd = `java -jar ${jar}${lang}${disabled} --json ${tempFile}` 72 | 73 | let response 74 | let result 75 | 76 | try { 77 | response = execSync(cmd, { stdio: "pipe" }) 78 | response = response.toString().split("\n") 79 | result = JSON.parse(response[response.length - 1]) 80 | } catch (e) { 81 | removeTempFile(tempFile) 82 | 83 | console.log(kleur.red("Cannot execute command via local LanguageTool cmd")) 84 | console.log("Please check if your command if valid.") 85 | process.exit(1) 86 | } 87 | 88 | removeTempFile(tempFile) 89 | 90 | const resultWithWords = { 91 | ...result, 92 | matches: removeFalsePositives( 93 | addWordFields(result.matches), 94 | cfg.dictionary, 95 | cfg.api_url === initialConfig.api_url ? disabledRules : [], 96 | ), 97 | } 98 | 99 | resultWithWords.matches.forEach((match) => { 100 | if (match.replacements.length > MAX_REPLACEMENTS) { 101 | match.replacements.length = MAX_REPLACEMENTS // eslint-disable-line 102 | } 103 | }) 104 | 105 | return resultWithWords 106 | } 107 | 108 | module.exports = checkViaCmd 109 | -------------------------------------------------------------------------------- /src/cli.test.js: -------------------------------------------------------------------------------- 1 | const shell = require("shelljs") 2 | const fs = require("fs") 3 | 4 | const prepareData = (text) => { 5 | if (!fs.existsSync("test-temp")) { 6 | fs.mkdirSync("test-temp") 7 | } 8 | 9 | fs.writeFileSync("test-temp/example.txt", text) 10 | } 11 | 12 | const removeData = () => { 13 | shell.rm("-rf", "test-temp") 14 | } 15 | 16 | describe("'listen' command", () => { 17 | it("prints potential mistakes with '--print' option", () => { 18 | const result = shell.exec( 19 | "node src/cli.js listen --print 'There is a mistkae'", 20 | ) 21 | 22 | expect(result.code).toEqual(1) 23 | expect(result.stderr).toEqual("") 24 | expect(result.grep("Context")).not.toEqual("") 25 | expect(result.grep("Suggested fix")).not.toEqual("") 26 | }) 27 | 28 | it("prints potential mistakes with '-p' option", () => { 29 | const result = shell.exec("node src/cli.js listen -p 'There is a mistkae'") 30 | 31 | expect(result.code).toEqual(1) 32 | expect(result.stderr).toEqual("") 33 | expect(result.grep("Context")).not.toEqual("") 34 | expect(result.grep("Suggested fix")).not.toEqual("") 35 | }) 36 | 37 | it("prints no mistakes with '--print' option", () => { 38 | const result = shell.exec( 39 | "node src/cli.js listen --print 'There are no mistakes'", 40 | ) 41 | 42 | expect(result.code).toEqual(0) 43 | expect(result.stderr).toEqual("") 44 | expect(result.grep("No mistakes found!")).not.toEqual("") 45 | }) 46 | 47 | it("prints no mistakes with '-p' option", () => { 48 | const result = shell.exec( 49 | "node src/cli.js listen -p 'There are no mistakes'", 50 | ) 51 | 52 | expect(result.code).toEqual(0) 53 | expect(result.stderr).toEqual("") 54 | expect(result.grep("No mistakes found!")).not.toEqual("") 55 | }) 56 | }) 57 | 58 | describe("'check' command", () => { 59 | it("prints potential mistakes with '--print' option", () => { 60 | prepareData("There is a mistkae") 61 | const result = shell.exec( 62 | "node src/cli.js check --print test-temp/example.txt", 63 | ) 64 | 65 | expect(result.code).toEqual(1) 66 | expect(result.stderr).toEqual("") 67 | expect(result.grep("Context")).not.toEqual("") 68 | expect(result.grep("Suggested fix")).not.toEqual("") 69 | removeData() 70 | }) 71 | 72 | it("prints potential mistakes with '-p' option", () => { 73 | prepareData("There is a mistkae") 74 | const result = shell.exec("node src/cli.js check -p test-temp/example.txt") 75 | 76 | expect(result.code).toEqual(1) 77 | expect(result.stderr).toEqual("") 78 | expect(result.grep("Context")).not.toEqual("") 79 | expect(result.grep("Suggested fix")).not.toEqual("") 80 | removeData() 81 | }) 82 | 83 | it("prints no mistakes with '--print' option", () => { 84 | prepareData("There are no mistakes") 85 | const result = shell.exec( 86 | "node src/cli.js check --print test-temp/example.txt", 87 | ) 88 | 89 | expect(result.code).toEqual(0) 90 | expect(result.stderr).toEqual("") 91 | expect(result.grep("No mistakes found!")).not.toEqual("") 92 | removeData() 93 | }) 94 | 95 | it("prints no mistakes with '-p' option", () => { 96 | prepareData("There are no mistakes") 97 | const result = shell.exec("node src/cli.js check -p test-temp/example.txt") 98 | 99 | expect(result.code).toEqual(0) 100 | expect(result.stderr).toEqual("") 101 | expect(result.grep("No mistakes found!")).not.toEqual("") 102 | removeData() 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /src/actions/checkInteractively.js: -------------------------------------------------------------------------------- 1 | const kleur = require("kleur") 2 | const checkWithFallback = require("../requests/checkWithFallback") 3 | const Mistake = require("../components/Mistake") 4 | const handleMistake = require("../prompts/handleMistake") 5 | const replaceAll = require("../text-manipulation/replaceAll") 6 | const equal = require("../utils/equal") 7 | const configure = require("./configure") 8 | const { displayUpdates } = require("../requests/updates") 9 | 10 | const checkInteractively = async (text, cfg) => { 11 | if (!text || text.trim().length === 0) { 12 | console.log(kleur.yellow("Nothing to check!")) 13 | return { changed: false } 14 | } 15 | 16 | const result = await checkWithFallback(text, cfg) 17 | 18 | if (result.matches.length === 0) { 19 | console.log(kleur.green("No mistakes found!")) 20 | await displayUpdates(cfg.paths.globalConfigDir) 21 | return { changed: false, text } 22 | } 23 | console.log( 24 | `Found ${result.matches.length} potential mistake${ 25 | result.matches.length === 1 ? "" : "s" 26 | }`, 27 | ) 28 | 29 | let { matches } = result 30 | const total = matches.length 31 | const transformations = [] 32 | 33 | while (matches.length > 0) { 34 | console.clear() 35 | console.log(`Language: ${result.language.name}`) 36 | console.log( 37 | `Resolved: ${total - matches.length} | Pending: ${matches.length}`, 38 | ) 39 | 40 | const currentMatch = matches.shift() 41 | console.log(Mistake(currentMatch)) 42 | 43 | // eslint-disable-next-line no-await-in-loop 44 | const { option, replacement } = await handleMistake( 45 | currentMatch.replacements, 46 | currentMatch.rule.issueType, 47 | ) 48 | 49 | if (option === "l") { 50 | configure("dictionary", currentMatch.word, cfg, false) 51 | } else if (option === "g") { 52 | configure("dictionary", currentMatch.word, cfg, true) 53 | } 54 | 55 | if (["i", "l", "g"].includes(option)) { 56 | matches = matches.filter((match) => { 57 | return !equal( 58 | [ 59 | match.message, 60 | match.shortMessage, 61 | match.replacements, 62 | match.type, 63 | match.rule, 64 | match.word, 65 | ], 66 | [ 67 | currentMatch.message, 68 | currentMatch.shortMessage, 69 | currentMatch.replacements, 70 | currentMatch.type, 71 | currentMatch.rule, 72 | currentMatch.word, 73 | ], 74 | ) 75 | }) 76 | } else if (option === "n") { 77 | matches.push(currentMatch) 78 | } else if (option === "0") { 79 | transformations.push({ 80 | change: replacement, 81 | offset: currentMatch.offset, 82 | length: currentMatch.length, 83 | }) 84 | } else { 85 | try { 86 | transformations.push({ 87 | change: currentMatch.replacements[Number(option) - 1].value, 88 | offset: currentMatch.offset, 89 | length: currentMatch.length, 90 | }) 91 | } catch (e) { 92 | // It prevents from displaying error when users aborts with Ctrl-c 93 | if (e.message === "Cannot read property 'value' of undefined") { 94 | console.clear() 95 | process.exit(0) 96 | } 97 | 98 | console.error(e) 99 | } 100 | } 101 | } 102 | 103 | return { changed: true, text: replaceAll(text, transformations) } 104 | } 105 | 106 | module.exports = checkInteractively 107 | -------------------------------------------------------------------------------- /src/boot/prepareConfig.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const path = require("path") 3 | const { platform, homedir } = require("os") 4 | const findUpSync = require("../utils/findUpSync").default 5 | const initialConfig = require("../initialConfig") 6 | 7 | const configBasePath = { 8 | linux: ".config", 9 | darwin: "Library/Preferences", 10 | win32: "AppData/Roaming", 11 | } 12 | 13 | const home = homedir() 14 | const globalConfigDir = path.join(home, configBasePath[platform()], "gramma") 15 | const globalConfigFile = path.join(globalConfigDir, "gramma.json") 16 | const localConfigFile = findUpSync(".gramma.json") 17 | 18 | if (!fs.existsSync(globalConfigDir)) { 19 | fs.mkdirSync(globalConfigDir, { recursive: true }) 20 | } 21 | 22 | if (!fs.existsSync(globalConfigFile)) { 23 | fs.writeFileSync(globalConfigFile, JSON.stringify(initialConfig, null, 2)) 24 | } 25 | 26 | const loadEnvironmentVariables = (configText) => { 27 | const items = configText.match(/\${[a-z0-9\-_.]*}/gi) 28 | 29 | if (!items) { 30 | return configText 31 | } 32 | 33 | let text = configText 34 | 35 | items.forEach((item) => { 36 | const envVarName = item.slice(2, -1) 37 | const envVar = process.env[envVarName] 38 | 39 | if (envVar) { 40 | text = text.replace(item, envVar) 41 | } 42 | }) 43 | 44 | return text 45 | } 46 | 47 | const prepareFileConfig = (filePath) => { 48 | if (!filePath) return null 49 | 50 | return fs.existsSync(filePath) 51 | ? JSON.parse(loadEnvironmentVariables(fs.readFileSync(filePath).toString())) 52 | : null 53 | } 54 | 55 | const prepareArgvConfig = ({ language, disable, enable, global, markdown }) => { 56 | const disabledRules = Array.isArray(disable) ? disable : [disable] 57 | const enabledRules = Array.isArray(enable) ? enable : [enable] 58 | const rules = {} 59 | 60 | disabledRules.forEach((rule) => { 61 | rules[rule] = false 62 | }) 63 | 64 | enabledRules.forEach((rule) => { 65 | rules[rule] = true 66 | }) 67 | 68 | return { 69 | language, 70 | rules, 71 | markdown, 72 | modifiers: { 73 | global, 74 | }, 75 | } 76 | } 77 | 78 | const prepareConfig = (paths) => (argv) => { 79 | const globalConfig = prepareFileConfig(paths.globalConfigFile) 80 | const localConfig = prepareFileConfig(paths.localConfigFile) 81 | 82 | if (localConfig && localConfig.api_url === "localhost") { 83 | localConfig.api_url = "inherit" 84 | } 85 | 86 | const argvConfig = prepareArgvConfig(argv) 87 | 88 | const fileConfig = localConfig || globalConfig || {} 89 | 90 | // File configs replace one another, 91 | // so user's and project's configs won't mix 92 | const cfg = { 93 | ...initialConfig, 94 | ...fileConfig, 95 | } 96 | 97 | // If local config has api_url set to 'inherit' 98 | // then Gramma will use global settings (if set) or initial settings. 99 | // This allows to use dynamic url of the local server 100 | // eslint-disable-next-line camelcase 101 | const api_url = 102 | cfg.api_url === "inherit" 103 | ? (globalConfig || {}).api_url || initialConfig.api_url 104 | : cfg.api_url 105 | 106 | // Argv config alters nested values, 107 | // so you can change some rules for specific checks, 108 | // without erasing other rules defined in config files 109 | const sessionConfig = { 110 | ...cfg, 111 | language: 112 | argvConfig.language === "config" ? cfg.language : argvConfig.language, 113 | rules: { ...cfg.rules, ...argvConfig.rules }, 114 | modifiers: argvConfig.modifiers, 115 | api_url, 116 | markdown: argvConfig.markdown, 117 | } 118 | 119 | return { 120 | initial: initialConfig, 121 | global: globalConfig || {}, 122 | local: localConfig || {}, 123 | session: sessionConfig, 124 | paths, 125 | } 126 | } 127 | 128 | module.exports = prepareConfig({ 129 | globalConfigDir, 130 | globalConfigFile, 131 | localConfigFile, 132 | home, 133 | serverDownload: "https://languagetool.org/download/LanguageTool-stable.zip", 134 | }) 135 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require("dotenv").config() 3 | require("isomorphic-fetch") 4 | 5 | const yargs = require("yargs") 6 | const { version } = require("../package.json") 7 | const load = require("./boot/load") 8 | 9 | const check = require("./commands/check") 10 | const listen = require("./commands/listen") 11 | const commit = require("./commands/commit") 12 | const init = require("./commands/init") 13 | const config = require("./commands/config") 14 | const paths = require("./commands/paths") 15 | const server = require("./commands/server") 16 | const { hook } = require("./commands/hook") 17 | 18 | const { languageOptions } = require("./validators/languages") 19 | const { ruleOptions } = require("./validators/rules") 20 | 21 | // eslint-disable-next-line no-unused-expressions 22 | yargs 23 | .command( 24 | "check ", 25 | "check file for writing mistakes", 26 | (yargsCtx) => { 27 | yargsCtx.positional("text", { 28 | describe: "file to check", 29 | }) 30 | }, 31 | load(check), 32 | ) 33 | .command( 34 | "listen ", 35 | "check text for writing mistakes", 36 | (yargsCtx) => { 37 | yargsCtx.positional("text", { 38 | describe: "text to check", 39 | }) 40 | }, 41 | load(listen), 42 | ) 43 | .command( 44 | "commit ", 45 | "git commit -m with grammar check", 46 | (yargsCtx) => { 47 | yargsCtx.positional("text", { 48 | describe: "commit message to check", 49 | }) 50 | }, 51 | load(commit), 52 | ) 53 | .command( 54 | "hook", 55 | "toggles Git hook", 56 | (yargsCtx) => { 57 | yargsCtx.positional("text", { 58 | describe: "commit message file", 59 | }) 60 | }, 61 | load(hook), 62 | ) 63 | .command( 64 | "init", 65 | "create local config with default settings", 66 | () => {}, 67 | load(init), 68 | ) 69 | .command( 70 | "config ", 71 | "set config entry", 72 | (yargsCtx) => { 73 | yargsCtx 74 | .positional("key", { 75 | describe: "name of the config entry", 76 | }) 77 | .positional("value", { 78 | describe: "value of the config entry", 79 | }) 80 | }, 81 | load(config), 82 | ) 83 | .command("paths", "show paths used by Gramma", () => {}, load(paths)) 84 | .command( 85 | "server ", 86 | "manage local API server", 87 | (yargsCtx) => { 88 | yargsCtx.positional("action", { 89 | describe: "action to take (install / start / stop / pid / gui)", 90 | }) 91 | }, 92 | load(server), 93 | ) 94 | .alias("help", "h") 95 | .version(`v${version}`) 96 | .alias("version", "v") 97 | .hide("paths") 98 | .option("print", { 99 | alias: "p", 100 | type: "boolean", 101 | default: false, 102 | describe: "Print mistakes non-interactively", 103 | }) 104 | .option("no-colors", { 105 | alias: "n", 106 | type: "boolean", 107 | default: false, 108 | describe: "Disable output colors", 109 | }) 110 | .option("language", { 111 | alias: "l", 112 | type: "string", 113 | default: "config", 114 | describe: "Set the language of the text", 115 | choices: languageOptions, 116 | }) 117 | .option("disable", { 118 | alias: "d", 119 | type: "string", 120 | describe: "Disable specific rule", 121 | default: [], 122 | choices: ruleOptions, 123 | }) 124 | .option("enable", { 125 | alias: "e", 126 | type: "string", 127 | describe: "Enable specific rule", 128 | default: [], 129 | choices: ruleOptions, 130 | }) 131 | .option("all", { 132 | alias: "a", 133 | type: "boolean", 134 | default: false, 135 | describe: "Add -a flag to git commit command", 136 | }) 137 | .option("global", { 138 | alias: "g", 139 | type: "boolean", 140 | default: false, 141 | describe: "Use global configuration file with 'config' command", 142 | }) 143 | .option("markdown", { 144 | alias: "m", 145 | type: "boolean", 146 | default: false, 147 | describe: "Treat the text as markdown", 148 | }) 149 | .option("port", { 150 | type: "number", 151 | describe: "Set the port number of local API server", 152 | }) 153 | .demandCommand().argv 154 | -------------------------------------------------------------------------------- /src/commands/hook.js: -------------------------------------------------------------------------------- 1 | const kleur = require("kleur") 2 | const fs = require("fs") 3 | const path = require("path") 4 | const os = require("os") 5 | const { execSync } = require("child_process") 6 | const checkInteractively = require("../actions/checkInteractively") 7 | const saveNow = require("../actions/saveNow") 8 | const appLocation = require("../utils/appLocation") 9 | 10 | const sys = os.platform() 11 | 12 | const REDIRECT_STDIN = "\n\nexec < /dev/tty" 13 | 14 | const getHookCode = (command, stdin = true) => { 15 | const stdinCode = stdin ? REDIRECT_STDIN : "" 16 | 17 | return { 18 | linux: { 19 | full: `#!/bin/sh${stdinCode}\n\n${command}\n`, 20 | partial: `${stdinCode}\n\n${command}\n`, 21 | }, 22 | darwin: { 23 | full: `#!/bin/sh${stdinCode}\n\n${command}\n`, 24 | partial: `${stdinCode}\n\n${command}\n`, 25 | }, 26 | win32: { 27 | full: `#!/bin/sh${stdinCode}\n\n${command}\n`.replace(/\\/g, "/"), 28 | partial: `${stdinCode}\n\n${command}\n`.replace(/\\/g, "/"), 29 | }, 30 | } 31 | } 32 | 33 | const gitRoot = path.join(process.cwd(), ".git") 34 | 35 | const checkGit = () => { 36 | return fs.existsSync(gitRoot) 37 | } 38 | 39 | const createEmptyFile = (file) => { 40 | if (!fs.existsSync(file)) { 41 | fs.closeSync(fs.openSync(file, "w")) 42 | } 43 | } 44 | 45 | const addHookCode = (hookFile, hookCode, onlyCreate, name) => { 46 | if (fs.existsSync(hookFile)) { 47 | const content = fs.readFileSync(hookFile).toString() 48 | const alreadyExists = content.includes(hookCode[sys].partial) 49 | 50 | if (alreadyExists && !onlyCreate) { 51 | const newContent = content.replace(hookCode[sys].partial, "") 52 | fs.writeFileSync(hookFile, newContent) 53 | console.log(kleur.green(`Hook (${name}) removed!`)) 54 | } else if (alreadyExists) { 55 | console.log(kleur.yellow(`Hook (${name}) already exists!`)) 56 | } else { 57 | fs.appendFileSync(hookFile, hookCode[sys].partial) 58 | console.log(kleur.green(`Hook (${name}) created!`)) 59 | } 60 | } else { 61 | fs.writeFileSync(hookFile, hookCode[sys].full) 62 | fs.chmodSync(hookFile, "755") 63 | console.log(kleur.green(`Hook (${name}) created!`)) 64 | } 65 | } 66 | 67 | const addHooksCode = (onlyCreate = false) => { 68 | const hasGit = checkGit() 69 | 70 | if (!hasGit) { 71 | console.log(kleur.red("No .git in this directory")) 72 | process.exit(1) 73 | } 74 | 75 | const hooksConfig = fs 76 | .readFileSync(path.join(gitRoot, "config")) 77 | .toString() 78 | .match(/hooksPath *=.*/gi) 79 | 80 | const hooksFolder = hooksConfig && hooksConfig[0].split("=")[1].trim() 81 | 82 | const hookFileCommitMsg = hooksFolder 83 | ? path.resolve(process.cwd(), hooksFolder, "commit-msg") 84 | : path.resolve(process.cwd(), ".git", "hooks", "commit-msg") 85 | 86 | const hookFilePostCommit = hooksFolder 87 | ? path.resolve(process.cwd(), hooksFolder, "post-commit") 88 | : path.resolve(process.cwd(), ".git", "hooks", "post-commit") 89 | 90 | const commandCommitMsg = fs.existsSync("node_modules") 91 | ? "npx gramma hook $1" 92 | : `${appLocation} hook $1` 93 | 94 | const commandPostCommit = fs.existsSync("node_modules") 95 | ? "npx gramma hook cleanup" 96 | : `${appLocation} hook cleanup` 97 | 98 | const hookCodeCommitMsg = getHookCode(commandCommitMsg) 99 | const hookCodePostCommit = getHookCode(commandPostCommit, false) 100 | 101 | addHookCode(hookFileCommitMsg, hookCodeCommitMsg, onlyCreate, "commit-msg") 102 | addHookCode(hookFilePostCommit, hookCodePostCommit, onlyCreate, "post-commit") 103 | } 104 | 105 | const hook = async (argv, cfg) => { 106 | const arg = 107 | process.argv[process.argv.length - 1] !== "hook" 108 | ? process.argv[process.argv.length - 1] 109 | : null 110 | 111 | // No arg - execute the default command 112 | if (!arg) { 113 | addHooksCode() 114 | process.exit() 115 | } 116 | 117 | // Temporary file to coordinate git hooks 118 | // See: https://stackoverflow.com/a/12802592/4713502 119 | const tempFile = path.join(cfg.paths.globalConfigDir, ".commit") 120 | 121 | // Code executed by `post-commit` hook 122 | if (arg === "cleanup") { 123 | if (cfg.paths.localConfigFile && fs.existsSync(tempFile)) { 124 | fs.unlinkSync(tempFile) 125 | 126 | try { 127 | execSync(`git add ${cfg.paths.localConfigFile}`) 128 | execSync(`git commit --amend --no-edit --no-verify`) 129 | } catch (e) {} // eslint-disable-line 130 | } 131 | 132 | process.exit() 133 | } 134 | 135 | // Code executed by `commit-msg` hook 136 | createEmptyFile(tempFile) 137 | 138 | const file = arg 139 | 140 | const commitText = fs 141 | .readFileSync(file) 142 | .toString() 143 | .replace(/# ------------------------ >8[\S\s]*/m, "") // Remove diff part on --verbose 144 | .replace(/#.*/g, "") // Remove other comments 145 | 146 | const { changed, text } = await checkInteractively(commitText, cfg) 147 | 148 | if (changed) { 149 | await saveNow(text, file) 150 | } 151 | 152 | process.exit() 153 | } 154 | 155 | exports.checkGit = checkGit 156 | exports.addHookCode = addHooksCode 157 | exports.hook = hook 158 | -------------------------------------------------------------------------------- /src/utils/findUpSync.js: -------------------------------------------------------------------------------- 1 | var __create = Object.create 2 | var __defProp = Object.defineProperty 3 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor 4 | var __getOwnPropNames = Object.getOwnPropertyNames 5 | var __getProtoOf = Object.getPrototypeOf 6 | var __hasOwnProp = Object.prototype.hasOwnProperty 7 | var __markAsModule = (target) => 8 | __defProp(target, "__esModule", { value: true }) 9 | var __export = (target, all) => { 10 | __markAsModule(target) 11 | for (var name in all) 12 | __defProp(target, name, { get: all[name], enumerable: true }) 13 | } 14 | var __reExport = (target, module2, desc) => { 15 | if ( 16 | (module2 && typeof module2 === "object") || 17 | typeof module2 === "function" 18 | ) { 19 | for (let key of __getOwnPropNames(module2)) 20 | if (!__hasOwnProp.call(target, key) && key !== "default") 21 | __defProp(target, key, { 22 | get: () => module2[key], 23 | enumerable: 24 | !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable, 25 | }) 26 | } 27 | return target 28 | } 29 | var __toModule = (module2) => { 30 | return __reExport( 31 | __markAsModule( 32 | __defProp( 33 | module2 != null ? __create(__getProtoOf(module2)) : {}, 34 | "default", 35 | module2 && module2.__esModule && "default" in module2 36 | ? { get: () => module2.default, enumerable: true } 37 | : { value: module2, enumerable: true }, 38 | ), 39 | ), 40 | module2, 41 | ) 42 | } 43 | 44 | // lib/findUpSync.mjs 45 | __export(exports, { 46 | default: () => findUpSync_default, 47 | }) 48 | 49 | // lib/node_modules/find-up/index.js 50 | var import_path = __toModule(require("path")) 51 | 52 | // lib/node_modules/locate-path/index.js 53 | var import_node_process = __toModule(require("process")) 54 | var import_node_path = __toModule(require("path")) 55 | var import_node_fs = __toModule(require("fs")) 56 | 57 | // lib/node_modules/yocto-queue/index.js 58 | var Node = class { 59 | value 60 | next 61 | constructor(value) { 62 | this.value = value 63 | } 64 | } 65 | var Queue = class { 66 | #head 67 | #tail 68 | #size 69 | constructor() { 70 | this.clear() 71 | } 72 | enqueue(value) { 73 | const node = new Node(value) 74 | if (this.#head) { 75 | this.#tail.next = node 76 | this.#tail = node 77 | } else { 78 | this.#head = node 79 | this.#tail = node 80 | } 81 | this.#size++ 82 | } 83 | dequeue() { 84 | const current = this.#head 85 | if (!current) { 86 | return 87 | } 88 | this.#head = this.#head.next 89 | this.#size-- 90 | return current.value 91 | } 92 | clear() { 93 | this.#head = void 0 94 | this.#tail = void 0 95 | this.#size = 0 96 | } 97 | get size() { 98 | return this.#size 99 | } 100 | *[Symbol.iterator]() { 101 | let current = this.#head 102 | while (current) { 103 | yield current.value 104 | current = current.next 105 | } 106 | } 107 | } 108 | 109 | // lib/node_modules/locate-path/index.js 110 | var typeMappings = { 111 | directory: "isDirectory", 112 | file: "isFile", 113 | } 114 | function checkType(type) { 115 | if (type in typeMappings) { 116 | return 117 | } 118 | throw new Error(`Invalid type specified: ${type}`) 119 | } 120 | var matchType = (type, stat) => type === void 0 || stat[typeMappings[type]]() 121 | function locatePathSync( 122 | paths, 123 | { 124 | cwd = import_node_process.default.cwd(), 125 | type = "file", 126 | allowSymlinks = true, 127 | } = {}, 128 | ) { 129 | checkType(type) 130 | const statFunction = allowSymlinks 131 | ? import_node_fs.default.statSync 132 | : import_node_fs.default.lstatSync 133 | for (const path_ of paths) { 134 | try { 135 | const stat = statFunction(import_node_path.default.resolve(cwd, path_)) 136 | if (matchType(type, stat)) { 137 | return path_ 138 | } 139 | } catch {} 140 | } 141 | } 142 | 143 | // lib/node_modules/path-exists/index.js 144 | var import_node_fs2 = __toModule(require("fs")) 145 | 146 | // lib/node_modules/find-up/index.js 147 | var findUpStop = Symbol("findUpStop") 148 | function findUpMultipleSync(name, options = {}) { 149 | let directory = import_path.default.resolve(options.cwd || "") 150 | const { root } = import_path.default.parse(directory) 151 | const stopAt = options.stopAt || root 152 | const limit = options.limit || Number.POSITIVE_INFINITY 153 | const paths = [name].flat() 154 | const runMatcher = (locateOptions) => { 155 | if (typeof name !== "function") { 156 | return locatePathSync(paths, locateOptions) 157 | } 158 | const foundPath = name(locateOptions.cwd) 159 | if (typeof foundPath === "string") { 160 | return locatePathSync([foundPath], locateOptions) 161 | } 162 | return foundPath 163 | } 164 | const matches = [] 165 | while (true) { 166 | const foundPath = runMatcher({ ...options, cwd: directory }) 167 | if (foundPath === findUpStop) { 168 | break 169 | } 170 | if (foundPath) { 171 | matches.push(import_path.default.resolve(directory, foundPath)) 172 | } 173 | if (directory === stopAt || matches.length >= limit) { 174 | break 175 | } 176 | directory = import_path.default.dirname(directory) 177 | } 178 | return matches 179 | } 180 | function findUpSync(name, options = {}) { 181 | const matches = findUpMultipleSync(name, { ...options, limit: 1 }) 182 | return matches[0] 183 | } 184 | 185 | // lib/findUpSync.mjs 186 | var findUpSync_default = findUpSync 187 | // Annotate the CommonJS export names for ESM import in node: 188 | 0 && (module.exports = {}) 189 | -------------------------------------------------------------------------------- /_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% if site.google_analytics %} 6 | 7 | 13 | {% endif %} 14 | 15 | 16 | Gramma - command-line grammar checker 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 |
v1.6.0
43 | 75 | 76 |
77 |
78 | Star 79 | Watch 80 | Issue 81 |
82 | 83 | {{ content }} 84 | 85 |
86 | Star 87 | Watch 88 | Issue 89 |
90 | 91 | 97 |
98 | 99 | 100 | -------------------------------------------------------------------------------- /data/languages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Arabic", 4 | "code": "ar", 5 | "longCode": "ar", 6 | "grammarbotIo": false, 7 | "languagetoolOrg": true 8 | }, 9 | { 10 | "name": "Asturian", 11 | "code": "ast", 12 | "longCode": "ast-ES", 13 | "grammarbotIo": true, 14 | "languagetoolOrg": true 15 | }, 16 | { 17 | "name": "Belarusian", 18 | "code": "be", 19 | "longCode": "be-BY", 20 | "grammarbotIo": true, 21 | "languagetoolOrg": true 22 | }, 23 | { 24 | "name": "Breton", 25 | "code": "br", 26 | "longCode": "br-FR", 27 | "grammarbotIo": true, 28 | "languagetoolOrg": true 29 | }, 30 | { 31 | "name": "Catalan", 32 | "code": "ca", 33 | "longCode": "ca-ES", 34 | "grammarbotIo": true, 35 | "languagetoolOrg": true 36 | }, 37 | { 38 | "name": "Catalan (Valencian)", 39 | "code": "ca", 40 | "longCode": "ca-ES-valencia", 41 | "grammarbotIo": true, 42 | "languagetoolOrg": true 43 | }, 44 | { 45 | "name": "Chinese", 46 | "code": "zh", 47 | "longCode": "zh-CN", 48 | "grammarbotIo": false, 49 | "languagetoolOrg": true 50 | }, 51 | { 52 | "name": "Danish", 53 | "code": "da", 54 | "longCode": "da-DK", 55 | "grammarbotIo": true, 56 | "languagetoolOrg": true 57 | }, 58 | { 59 | "name": "Dutch", 60 | "code": "nl", 61 | "longCode": "nl", 62 | "grammarbotIo": true, 63 | "languagetoolOrg": true 64 | }, 65 | { 66 | "name": "Dutch (Belgium)", 67 | "code": "nl", 68 | "longCode": "nl-BE", 69 | "grammarbotIo": false, 70 | "languagetoolOrg": true 71 | }, 72 | { 73 | "name": "English", 74 | "code": "en", 75 | "longCode": "en", 76 | "grammarbotIo": true, 77 | "languagetoolOrg": true 78 | }, 79 | { 80 | "name": "English (Australian)", 81 | "code": "en", 82 | "longCode": "en-AU", 83 | "grammarbotIo": true, 84 | "languagetoolOrg": true 85 | }, 86 | { 87 | "name": "English (Canadian)", 88 | "code": "en", 89 | "longCode": "en-CA", 90 | "grammarbotIo": true, 91 | "languagetoolOrg": true 92 | }, 93 | { 94 | "name": "English (GB)", 95 | "code": "en", 96 | "longCode": "en-GB", 97 | "grammarbotIo": true, 98 | "languagetoolOrg": true 99 | }, 100 | { 101 | "name": "English (New Zealand)", 102 | "code": "en", 103 | "longCode": "en-NZ", 104 | "grammarbotIo": true, 105 | "languagetoolOrg": true 106 | }, 107 | { 108 | "name": "English (South African)", 109 | "code": "en", 110 | "longCode": "en-ZA", 111 | "grammarbotIo": true, 112 | "languagetoolOrg": true 113 | }, 114 | { 115 | "name": "English (US)", 116 | "code": "en", 117 | "longCode": "en-US", 118 | "grammarbotIo": true, 119 | "languagetoolOrg": true 120 | }, 121 | { 122 | "name": "Esperanto", 123 | "code": "eo", 124 | "longCode": "eo", 125 | "grammarbotIo": true, 126 | "languagetoolOrg": true 127 | }, 128 | { 129 | "name": "French", 130 | "code": "fr", 131 | "longCode": "fr", 132 | "grammarbotIo": false, 133 | "languagetoolOrg": true 134 | }, 135 | { 136 | "name": "Galician", 137 | "code": "gl", 138 | "longCode": "gl-ES", 139 | "grammarbotIo": true, 140 | "languagetoolOrg": true 141 | }, 142 | { 143 | "name": "German", 144 | "code": "de", 145 | "longCode": "de", 146 | "grammarbotIo": false, 147 | "languagetoolOrg": true 148 | }, 149 | { 150 | "name": "German (Austria)", 151 | "code": "de", 152 | "longCode": "de-AT", 153 | "grammarbotIo": false, 154 | "languagetoolOrg": true 155 | }, 156 | { 157 | "name": "German (Germany)", 158 | "code": "de", 159 | "longCode": "de-DE", 160 | "grammarbotIo": false, 161 | "languagetoolOrg": true 162 | }, 163 | { 164 | "name": "German (Swiss)", 165 | "code": "de", 166 | "longCode": "de-CH", 167 | "grammarbotIo": false, 168 | "languagetoolOrg": true 169 | }, 170 | { 171 | "name": "Greek", 172 | "code": "el", 173 | "longCode": "el-GR", 174 | "grammarbotIo": true, 175 | "languagetoolOrg": true 176 | }, 177 | { 178 | "name": "Irish", 179 | "code": "ga", 180 | "longCode": "ga-IE", 181 | "grammarbotIo": false, 182 | "languagetoolOrg": true 183 | }, 184 | { 185 | "name": "Italian", 186 | "code": "it", 187 | "longCode": "it", 188 | "grammarbotIo": false, 189 | "languagetoolOrg": true 190 | }, 191 | { 192 | "name": "Japanese", 193 | "code": "ja", 194 | "longCode": "ja-JP", 195 | "grammarbotIo": true, 196 | "languagetoolOrg": true 197 | }, 198 | { 199 | "name": "Khmer", 200 | "code": "km", 201 | "longCode": "km-KH", 202 | "grammarbotIo": true, 203 | "languagetoolOrg": true 204 | }, 205 | { 206 | "name": "Persian", 207 | "code": "fa", 208 | "longCode": "fa", 209 | "grammarbotIo": true, 210 | "languagetoolOrg": true 211 | }, 212 | { 213 | "name": "Polish", 214 | "code": "pl", 215 | "longCode": "pl-PL", 216 | "grammarbotIo": true, 217 | "languagetoolOrg": true 218 | }, 219 | { 220 | "name": "Portuguese", 221 | "code": "pt", 222 | "longCode": "pt", 223 | "grammarbotIo": false, 224 | "languagetoolOrg": true 225 | }, 226 | { 227 | "name": "Portuguese (Angola preAO)", 228 | "code": "pt", 229 | "longCode": "pt-AO", 230 | "grammarbotIo": false, 231 | "languagetoolOrg": true 232 | }, 233 | { 234 | "name": "Portuguese (Brazil)", 235 | "code": "pt", 236 | "longCode": "pt-BR", 237 | "grammarbotIo": false, 238 | "languagetoolOrg": true 239 | }, 240 | { 241 | "name": "Portuguese (Moçambique preAO)", 242 | "code": "pt", 243 | "longCode": "pt-MZ", 244 | "grammarbotIo": false, 245 | "languagetoolOrg": true 246 | }, 247 | { 248 | "name": "Portuguese (Portugal)", 249 | "code": "pt", 250 | "longCode": "pt-PT", 251 | "grammarbotIo": false, 252 | "languagetoolOrg": true 253 | }, 254 | { 255 | "name": "Romanian", 256 | "code": "ro", 257 | "longCode": "ro-RO", 258 | "grammarbotIo": true, 259 | "languagetoolOrg": true 260 | }, 261 | { 262 | "name": "Russian", 263 | "code": "ru", 264 | "longCode": "ru-RU", 265 | "grammarbotIo": false, 266 | "languagetoolOrg": true 267 | }, 268 | { 269 | "name": "Simple German", 270 | "code": "de-DE-x-simple-language", 271 | "longCode": "de-DE-x-simple-language", 272 | "grammarbotIo": true, 273 | "languagetoolOrg": true 274 | }, 275 | { 276 | "name": "Slovak", 277 | "code": "sk", 278 | "longCode": "sk-SK", 279 | "grammarbotIo": true, 280 | "languagetoolOrg": true 281 | }, 282 | { 283 | "name": "Slovenian", 284 | "code": "sl", 285 | "longCode": "sl-SI", 286 | "grammarbotIo": true, 287 | "languagetoolOrg": true 288 | }, 289 | { 290 | "name": "Spanish", 291 | "code": "es", 292 | "longCode": "es", 293 | "grammarbotIo": false, 294 | "languagetoolOrg": true 295 | }, 296 | { 297 | "name": "Spanish (voseo)", 298 | "code": "es", 299 | "longCode": "es-AR", 300 | "grammarbotIo": false, 301 | "languagetoolOrg": true 302 | }, 303 | { 304 | "name": "Swedish", 305 | "code": "sv", 306 | "longCode": "sv", 307 | "grammarbotIo": true, 308 | "languagetoolOrg": true 309 | }, 310 | { 311 | "name": "Tagalog", 312 | "code": "tl", 313 | "longCode": "tl-PH", 314 | "grammarbotIo": true, 315 | "languagetoolOrg": true 316 | }, 317 | { 318 | "name": "Tamil", 319 | "code": "ta", 320 | "longCode": "ta-IN", 321 | "grammarbotIo": true, 322 | "languagetoolOrg": true 323 | }, 324 | { 325 | "name": "Ukrainian", 326 | "code": "uk", 327 | "longCode": "uk-UA", 328 | "grammarbotIo": true, 329 | "languagetoolOrg": true 330 | } 331 | ] -------------------------------------------------------------------------------- /assets/gramma-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/gramma-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | gramma-logo 3 |
4 |
5 | gramma-title 6 |
7 | 8 | 15 | 16 |
 
17 | 18 |
19 | CircleCI 20 | npm version 21 | node version 22 | npm license 23 |
24 | 25 |
 
26 | 27 |
28 | Example 29 |
30 | 31 |
 
32 |
---
33 | 34 | ## Features 35 | 36 | - Provides advanced grammar checks via LanguageTool (remote API or local server). 37 | - Supports global and local (per-project) configuration. 38 | - Supports plain text and markdown. 39 | - Git integration! 40 | - Fully interactive! 41 | 42 |
---
43 | 44 | ## Contents 45 | 46 | 1. [Installation](#installation) 47 | - [Via NPM (global)](#installation-npm) 48 | - [Standalone binary](#installation-binary) 49 | - [Dev tool for JS/TS projects](#installation-dev) 50 | - [Local LanguageTool server (optional)](#installation-server) 51 | 1. [Usage](#usage) 52 | - [Check file](#usage-check) 53 | - [Check string](#usage-listen) 54 | - [Git commit with grammar check](#usage-commit) 55 | - [Command-line options](#usage-options) 56 | - [Usage inside VIM](#usage-vim) 57 | 1. [Configuration](#config) 58 | - [Introduction](#config-intro) 59 | - [Local config](#config-local) 60 | - [Git integration](#config-git) 61 | - [Checker settings](#config-checker) 62 | - [Customizing API server](#config-server) 63 | - [Security](#config-security) 64 | 1. [Managing a local server](#server) 65 | 1. [JS API](#js) 66 | 1. [License](#license) 67 | 68 | 69 | 70 |
---
71 | 72 | ## Installation 73 | 74 | 75 | 76 | ### Via NPM 77 | 78 | It is the recommended way if you have Node.js already installed (or you are willing to do so). 79 | 80 | ``` 81 | npm i gramma -g 82 | ``` 83 | 84 |
85 | 86 | 87 | 88 | ### Standalone binary 89 | 90 | If you prefer a single binary file, you can download it for the most popular platforms: 91 | 92 | - [gramma-linux64-v1.6.0.zip](https://github.com/caderek/gramma/releases/download/v1.6.0/gramma-linux64-v1.6.0.zip) 93 | - [gramma-macos-v1.6.0.zip](https://github.com/caderek/gramma/releases/download/v1.6.0/gramma-macos-v1.6.0.zip) 94 | - [gramma-windows64-v1.6.0.zip](https://github.com/caderek/gramma/releases/download/v1.6.0/gramma-windows64-v1.6.0.zip) 95 | 96 | After downloading and unpacking the binary, add it to your PATH or create a symlink to your executable directory (depending on the platform). 97 | 98 |
99 | 100 | 101 | 102 | ### Dev tool for JS/TS projects 103 | 104 | You can install Gramma locally for your JS/TS project - this method gives you a separate, project specific config. 105 | 106 | ``` 107 | npm i gramma -D 108 | ``` 109 | 110 | or 111 | 112 | ``` 113 | yarn add gramma -D 114 | ``` 115 | 116 | Then create the local config file: 117 | 118 | ``` 119 | npx gramma init 120 | ``` 121 | 122 | You will be asked if you want to integrate Gramma with Git (via hook). You can later manually toggle git hook via `npx gramma hook` command. 123 | 124 | Git hook also works with a non-default hooks path (Husky, etc.). 125 | 126 |
127 | 128 | 129 | 130 | ### Local LanguageTool server (optional) 131 | 132 | For this to work, you have to install Java 1.8 or higher (you can find it [here](https://adoptium.net)). You can check if you have it installed already by running: 133 | 134 | ``` 135 | java -version 136 | ``` 137 | 138 | To install the local server, use: 139 | 140 | ``` 141 | gramma server install 142 | ``` 143 | 144 | That's it - Gramma will now use and manage the local server automatically. 145 | 146 | 147 | 148 |
---
149 | 150 | ## Usage 151 | 152 | 153 | 154 | ### Check file 155 | 156 | Interactive fix: 157 | 158 | ``` 159 | gramma check [file] 160 | ``` 161 | 162 | Just print potential mistakes and return status code: 163 | 164 | ``` 165 | gramma check -p [file] 166 | ``` 167 | 168 | Examples: 169 | 170 | ``` 171 | gramma check path/to/my_file.txt 172 | ``` 173 | 174 | ``` 175 | gramma check -p path/to/other/file.txt 176 | ``` 177 | 178 |
179 | 180 | 181 | 182 | ### Check string 183 | 184 | Interactive fix: 185 | 186 | ``` 187 | gramma listen [text] 188 | ``` 189 | 190 | Just print potential mistakes and return status code: 191 | 192 | ``` 193 | gramma listen -p [text] 194 | ``` 195 | 196 | Examples: 197 | 198 | ``` 199 | gramma listen "This sentence will be checked interactively." 200 | ``` 201 | 202 | ``` 203 | gramma listen -p "Suggestions for this sentence will be printed." 204 | ``` 205 | 206 |
207 | 208 | 209 | 210 | ### Git commit with grammar check 211 | 212 | _**TIP:** Instead of the commands below, you can use [Git integration](#config-git)._ 213 | 214 | Equivalent to `git commit -m [message]`: 215 | 216 | ``` 217 | gramma commit [text] 218 | ``` 219 | 220 | Equivalent to `git commit -am [message]`: 221 | 222 | ``` 223 | gramma commit -a [text] 224 | ``` 225 | 226 | Examples: 227 | 228 | ``` 229 | gramma commit "My commit message" 230 | ``` 231 | 232 | ``` 233 | gramma commit -a "Another commit message (files added)" 234 | ``` 235 | 236 |
237 | 238 | 239 | 240 | ### Command-line options 241 | 242 | _Note: This section describes options for grammar-checking commands only. Other command-specific options are described in their specific sections of this document._ 243 | 244 | - `-p / --print` - check text in the non-interactive mode 245 | - `-n / --no-colors` - when paired with the `-p` flag, removes colors from the output 246 | - `-d / --disable ` - disable specific [rule](#available-rules) 247 | - `-e / --enable ` - enable specific [rule](#available-rules) 248 | - `-l / --language ` - mark a text as written in provided [language](#available-languages) 249 | - `-m / --markdown` - treat the input as markdown (removes some false-positives) 250 | 251 | You can enable or disable multiple rules in one command by using a corresponding option multiple times. You can also compound boolean options if you use their short version. 252 | 253 | Example: 254 | 255 | ``` 256 | gramma listen "I like making mistkaes!" -pn -d typos -d typography -e casing -l en-GB 257 | ``` 258 | 259 |
260 | 261 | 262 | 263 | ### Usage inside VIM 264 | 265 | If you are a VIM/Neovim user, you can use Gramma directly inside the editor: 266 | 267 | Print the potential mistakes: 268 | 269 | ``` 270 | :w !gramma check /dev/stdin -pn 271 | ``` 272 | 273 | Interactive fix of the current file: 274 | 275 | ``` 276 | :terminal gramma check % 277 | ``` 278 | 279 | It will open the interactive terminal inside VIM - to handle Gramma suggestions, enter the interactive mode (`a` or `i`) and use Gramma as usual. After you fix the mistakes and replace a file, press `Enter` to return to the editor. 280 | 281 |
282 | Example GIF (click to expand) 283 | Gramma VIM example 284 |
285 | 286 | 287 | 288 |
---
289 | 290 | ## Configuration 291 | 292 | 293 | 294 | ### Introduction 295 | 296 | With Gramma, you can use a global and local configuration file. Gramma will use a proper config file following their priority: 297 | 298 | 1. Command-line options 299 | 2. Local config 300 | 3. Global config 301 | 302 | Gramma will automatically generate a global configuration file on the first run. 303 | 304 | You can check the path to the global configuration file (as well as other paths used by Gramma) via the following command: 305 | 306 | ``` 307 | gramma paths 308 | ``` 309 | 310 | You can change your settings by manually editing configuration files or running: 311 | 312 | ``` 313 | gramma config [-g] 314 | ``` 315 | 316 | _Note: `-g` (`--global`) flag should be used when you want to alter the global config._ 317 | 318 |
319 | 320 | 321 | 322 | ### Local config 323 | 324 | You can initialize local config by running the following command in your project's root directory: 325 | 326 | ``` 327 | gramma init 328 | ``` 329 | 330 | Gramma creates the local configuration file in your working directory under `.gramma.json` name. 331 | 332 |
333 | 334 | 335 | 336 | ### Git integration 337 | 338 | You can toggle Git hook via: 339 | 340 | ``` 341 | gramma hook 342 | ``` 343 | 344 | It will add/remove an entry in `commit-msg` hook. 345 | 346 | Gramma follows the Git configuration file, so it should work with a non-standard hooks location. 347 | 348 |
349 | 350 | 351 | 352 | ### Checker settings 353 | 354 | #### Adding a word to the dictionary 355 | 356 | Usually, you will add custom words to the local or global dictionary via interactive menu during the fix process, but you can also make it via separate command: 357 | 358 | ``` 359 | gramma config dictionary [-g] 360 | ``` 361 | 362 | Examples: 363 | 364 | ``` 365 | gramma config dictionary aws 366 | gramma config dictionary figma -g 367 | ``` 368 | 369 | #### Changing default language 370 | 371 | ``` 372 | gramma config language [-g] 373 | ``` 374 | 375 | Examples: 376 | 377 | ``` 378 | gramma config language en-GB 379 | gramma config language pl-PL -g 380 | ``` 381 | 382 | 383 | 384 |
385 | Available languages (click to expand) 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 |
CodeNamelanguagetool.orggrammarbot.iolocal
autoautomatic language detection
arArabic-
ast-ESAsturian
be-BYBelarusian
br-FRBreton
ca-ESCatalan
ca-ES-valenciaCatalan (Valencian)
zh-CNChinese-
da-DKDanish
nlDutch
nl-BEDutch (Belgium)-
enEnglish
en-AUEnglish (Australian)
en-CAEnglish (Canadian)
en-GBEnglish (GB)
en-NZEnglish (New Zealand)
en-ZAEnglish (South African)
en-USEnglish (US)
eoEsperanto
frFrench-
gl-ESGalician
deGerman-
de-ATGerman (Austria)-
de-DEGerman (Germany)-
de-CHGerman (Swiss)-
el-GRGreek
ga-IEIrish-
itItalian-
ja-JPJapanese
km-KHKhmer
faPersian
pl-PLPolish
ptPortuguese-
pt-AOPortuguese (Angola preAO)-
pt-BRPortuguese (Brazil)-
pt-MZPortuguese (Moçambique preAO)-
pt-PTPortuguese (Portugal)-
ro-RORomanian
ru-RURussian-
de-DE-x-simple-languageSimple German
sk-SKSlovak
sl-SISlovenian
esSpanish-
es-ARSpanish (voseo)-
svSwedish
tl-PHTagalog
ta-INTamil
uk-UAUkrainian
733 |
734 | 735 | _Note: By default, Gramma uses US English (`en-US`)._ 736 | 737 | #### Enabling and disabling rules 738 | 739 | Enabling a specific rule: 740 | 741 | ``` 742 | gramma config enable [-g] 743 | ``` 744 | 745 | Disabling a specific rule: 746 | 747 | ``` 748 | gramma config disable [-g] 749 | ``` 750 | 751 | Examples: 752 | 753 | ``` 754 | gramma config enable punctuation 755 | gramma config enable casing -g 756 | 757 | gramma config disable typography 758 | gramma config disable style -g 759 | ``` 760 | 761 | 762 | 763 |
764 | Available rules (click to expand) 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 |
RuleDescription
casingRules about detecting uppercase words where lowercase is required and vice versa.
colloquialismsColloquial style.
compoundingRules about spelling terms as one word or as as separate words.
confused_wordsWords that are easily confused, like 'there' and 'their' in English.
false_friendsFalse friends: words easily confused by language learners because a similar word exists in their native language.
gender_neutralityHelps to ensure gender-neutral terms.
grammarBasic grammar check.
miscMiscellaneous rules that don't fit elsewhere.
punctuationPunctuation mistakes.
redundancyRedundant words.
regionalismsRegionalisms: words used only in another language variant or used with different meanings.
repetitionsRepeated words.
semanticsLogic, content, and consistency problems.
styleGeneral style issues not covered by other categories, like overly verbose wording.
typographyProblems like incorrectly used dash or quote characters.
typosSpelling issues.
784 |
785 | 786 | _Note: By default, all rules are enabled._ 787 | 788 |
789 | 790 | 791 | 792 | ### Customizing API server 793 | 794 | #### Defining custom API endpoint 795 | 796 | If you want to use remote LanguageTool server, or use the one already installed in your system (not installed via `gramma server install`), you can define a custom API endpoint: 797 | 798 | ``` 799 | gramma config api_url [-g] 800 | ``` 801 | 802 | Examples: 803 | 804 | ``` 805 | gramma config api_url https://my-custom-api-url.xyz/v2/check 806 | gramma config api_url http://localhost:8081/v2/check -g 807 | ``` 808 | 809 | #### Running local server only when needed 810 | 811 | If you do not want the local server to run all the time, you can configure Gramma to run it only when needed (`run → check → close`). It is useful when you run Gramma only from time to time and want to lower the memory consumption: 812 | 813 | ``` 814 | gramma config server_once true -g 815 | 816 | ``` 817 | 818 | Revert: 819 | 820 | ``` 821 | gramma config server_once false -g 822 | ``` 823 | 824 | #### Adding API key 825 | 826 | If you use a paid option on [grammarbot.io](https://www.grammarbot.io/) or [languagetool.org](https://languagetool.org), you will receive an API key that you can use in Gramma: 827 | 828 | ``` 829 | gramma config api_key [-g] 830 | ``` 831 | 832 |
833 | 834 | 835 | 836 | ### Security 837 | 838 | If you need to store some sensitive data in your local config file (API key etc.) you can use environment variables directly in the config file (supports `.env` files). 839 | 840 | Example: 841 | 842 | ```json 843 | { 844 | "api_url": "https://my-language-tool-api.com/v2/check", 845 | "api_key": "${MY_ENV_VARIABLE}", 846 | ...other_settings 847 | } 848 | ``` 849 | 850 | _Note: The default API (`api.languagetool.org`) is generally [safe and does not store your texts](https://languagetool.org/pl/legal/privacy), but if you want to be extra careful, you should use a [local server](#installation-server) or custom API endpoint._ 851 | 852 | 853 | 854 |
---
855 | 856 | ## Managing a local server 857 | 858 | If you have [configured a local server](#installation-server), Gramma will manage the server automatically - nevertheless, there might be situations when you want to manage the server manually. Gramma simplifies this by exposing basic server commands: 859 | 860 | #### Starting the server 861 | 862 | ``` 863 | gramma server start 864 | ``` 865 | 866 | You can also specify a custom port: 867 | 868 | ``` 869 | gramma server start --port 870 | ``` 871 | 872 | _Note: When you use this command, Gramma will ignore the `server_once` config option. This is expected behavior - I assume that if you use this command, you want the server to actually run, not stop after the first check._ 873 | 874 | #### Stopping the server 875 | 876 | ``` 877 | gramma server stop 878 | ``` 879 | 880 | #### Getting the server info 881 | 882 | ``` 883 | gramma server info 884 | ``` 885 | 886 | #### Getting the server PID 887 | 888 | ``` 889 | gramma server pid 890 | ``` 891 | 892 | _Note: You can use `gramma server info` instead - this command is kept to not break backward compatibility._ 893 | 894 | #### Opening the built-in GUI 895 | 896 | ``` 897 | gramma server gui 898 | ``` 899 | 900 | 901 | 902 |
---
903 | 904 | ## JS API 905 | 906 | In addition to command-line usage, you can use two exposed methods if you want to handle mistakes by yourself. 907 | 908 | #### Imports 909 | 910 | If you use Node.js or a bundler for your browser build, you can use CommonJS or esm: 911 | 912 | ```js 913 | const gramma = require("gramma") 914 | ``` 915 | 916 | ```js 917 | import gramma from "gramma" 918 | ``` 919 | 920 | If you don't use a bundler and want to use gramma in the browser, there are some prebuild packages in [/bundle](https://github.com/caderek/gramma/tree/master/bundle) directory: 921 | 922 | - `gramma.esm.js` - ES Modules bundle 923 | - `gramma.esm.min.js` - minified ES Modules bundle 924 | - `gramma.min.js` - IIFE bundle exposing global `gramma` variable 925 | 926 | You can also import ESM bundle directly from CDN: 927 | 928 | ```html 929 | 932 | ``` 933 | 934 |
935 | 936 | #### check() method 937 | 938 | Returns a promise with a check result. 939 | 940 | ```js 941 | const gramma = require("gramma") 942 | 943 | gramma.check("Some text to check.").then(console.log) 944 | ``` 945 | 946 | You can also pass a second argument - an options object. Available options: 947 | 948 | - `api_url` - url to a non-default API server 949 | - `api_key` - server API key 950 | - `dictionary` - an array of words that should be whitelisted 951 | - `language` - language code to specify the text language 952 | - `rules` - object defining which rules should be disabled 953 | 954 |
955 | Default options object (click to expand) 956 |
 957 | {
 958 |   "api_url": "https://api.languagetool.org/v2/check",
 959 |   "api_key": "",
 960 |   "dictionary": [],
 961 |   "language": "en-US",
 962 |   "rules": {
 963 |     "casing": true,
 964 |     "colloquialisms": true,
 965 |     "compounding": true,
 966 |     "confused_words": true,
 967 |     "false_friends": true,
 968 |     "gender_neutrality": true,
 969 |     "grammar": true,
 970 |     "misc": true,
 971 |     "punctuation": true,
 972 |     "redundancy": true,
 973 |     "regionalisms": true,
 974 |     "repetitions": true,
 975 |     "semantics": true,
 976 |     "style": true,
 977 |     "typography": true,
 978 |     "typos": true
 979 |   }
 980 | }
 981 | 
982 |
983 | 984 | You can find all available values for each setting in the [configuration section](#config) of this document. 985 | 986 | Example with all options set: 987 | 988 | ```js 989 | const gramma = require("gramma") 990 | 991 | gramma 992 | .check("Some text to check.", { 993 | api_url: "http://my-custom-language-tool-server.xyz/v2/check", 994 | api_key: "SOME_API_KEY", 995 | dictionary: ["npm", "gramma"], 996 | language: "pl-PL", 997 | rules: { 998 | typography: false, 999 | casing: false, 1000 | }, 1001 | }) 1002 | .then(console.log) 1003 | ``` 1004 | 1005 |
1006 | 1007 | #### replaceAll() method 1008 | 1009 | Replace words with provided ones. It takes an array of objects in the following format: 1010 | 1011 | ```js 1012 | const exampleReplacements = [ 1013 | { offset: 6, length: 3, change: "correct phrase" }, 1014 | { offset: 20, length: 7, change: "another phrase" }, 1015 | ] 1016 | ``` 1017 | 1018 | You can find proper `offset` and `length` values in the object returned by the `check()` method. 1019 | 1020 | Example usage: 1021 | 1022 | ```js 1023 | const gramma = require("gramma") 1024 | 1025 | /** Your custom function **/ 1026 | const prepareReplacements = (matches) => { 1027 | // your code... 1028 | } 1029 | 1030 | const fix = async (text) => { 1031 | const { matches } = await gramma.check(text) 1032 | const replacements = prepareReplacements(matches) 1033 | 1034 | return gramma.replaceAll(text, replacements) 1035 | } 1036 | 1037 | const main = () => { 1038 | const correctText = await fix("Some text to check") 1039 | console.log(correctText) 1040 | } 1041 | 1042 | main() 1043 | ``` 1044 | 1045 | 1046 | 1047 |
---
1048 | 1049 | ## License 1050 | 1051 | The project is under open, non-restrictive [ISC license](https://github.com/caderek/gramma/blob/master/LICENSE.md). 1052 | --------------------------------------------------------------------------------