├── .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