subedit 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. {subedit-0.4.0 → subedit-0.5.0}/.vale.ini +0 -1
  2. {subedit-0.4.0 → subedit-0.5.0}/PKG-INFO +19 -13
  3. {subedit-0.4.0 → subedit-0.5.0}/README.md +18 -12
  4. subedit-0.5.0/docs/rules.md +105 -0
  5. {subedit-0.4.0 → subedit-0.5.0}/plugins/subedit/.claude-plugin/plugin.json +1 -1
  6. {subedit-0.4.0 → subedit-0.5.0}/plugins/subedit/scripts/run-subedit-hook.sh +1 -1
  7. {subedit-0.4.0 → subedit-0.5.0}/plugins/subedit/skills/judging-findings/SKILL.md +7 -6
  8. {subedit-0.4.0 → subedit-0.5.0}/pyproject.toml +1 -1
  9. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/__init__.py +1 -1
  10. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/application/corpus_meter.py +2 -2
  11. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/__init__.py +12 -0
  12. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/models/__init__.py +7 -0
  13. subedit-0.5.0/src/subedit/domain/models/abbreviations.py +64 -0
  14. subedit-0.5.0/src/subedit/domain/models/cldr_sentence_suppressions.txt +194 -0
  15. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/models/comment.py +38 -32
  16. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/models/diagnostic.py +3 -0
  17. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/models/markdown.py +6 -1
  18. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/models/prose.py +29 -25
  19. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/models/prose_unit.py +10 -0
  20. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/models/reflow.py +77 -69
  21. subedit-0.5.0/src/subedit/domain/models/rule_option.py +107 -0
  22. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/models/rule_set.py +6 -9
  23. subedit-0.5.0/src/subedit/domain/models/title_case.py +115 -0
  24. subedit-0.5.0/src/subedit/domain/models/word_classes.py +212 -0
  25. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/rules/__init__.py +13 -8
  26. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/rules/layout.py +3 -3
  27. subedit-0.5.0/src/subedit/domain/rules/markdown.py +226 -0
  28. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/rules/sentence.py +56 -32
  29. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/rules/structure.py +11 -9
  30. subedit-0.5.0/src/subedit/domain/rules/style.py +364 -0
  31. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/__init__.py +16 -10
  32. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/config_sources/__init__.py +2 -1
  33. subedit-0.5.0/src/subedit/infrastructure/config_sources/config_schema.py +86 -0
  34. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/config_sources/toml_config_source.py +67 -12
  35. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/parsers/markdown_parser.py +24 -2
  36. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/presentation/cli.py +21 -4
  37. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/presentation/hook.py +17 -4
  38. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/vale/vale.ini +0 -1
  39. {subedit-0.4.0 → subedit-0.5.0}/subedit.toml +3 -3
  40. {subedit-0.4.0 → subedit-0.5.0}/tests/application/test_corpus_meter.py +1 -1
  41. {subedit-0.4.0 → subedit-0.5.0}/tests/application/test_fixer.py +2 -2
  42. {subedit-0.4.0 → subedit-0.5.0}/tests/application/test_linter.py +0 -8
  43. {subedit-0.4.0 → subedit-0.5.0}/tests/domain/test_diagnostic.py +1 -1
  44. {subedit-0.4.0 → subedit-0.5.0}/tests/domain/test_markdown.py +13 -6
  45. {subedit-0.4.0 → subedit-0.5.0}/tests/domain/test_prose.py +76 -13
  46. subedit-0.5.0/tests/domain/test_rule_option.py +52 -0
  47. {subedit-0.4.0 → subedit-0.5.0}/tests/domain/test_rules_layout.py +3 -2
  48. {subedit-0.4.0 → subedit-0.5.0}/tests/domain/test_rules_sentence.py +56 -21
  49. {subedit-0.4.0 → subedit-0.5.0}/tests/domain/test_rules_structure.py +8 -6
  50. subedit-0.5.0/tests/domain/test_style.py +448 -0
  51. {subedit-0.4.0 → subedit-0.5.0}/tests/infrastructure/test_config.py +96 -15
  52. {subedit-0.4.0 → subedit-0.5.0}/tests/infrastructure/test_schema.py +13 -20
  53. {subedit-0.4.0 → subedit-0.5.0}/tests/infrastructure/test_vale_style.py +0 -1
  54. {subedit-0.4.0 → subedit-0.5.0}/tests/presentation/test_cli.py +30 -18
  55. {subedit-0.4.0 → subedit-0.5.0}/tests/presentation/test_hook.py +9 -0
  56. {subedit-0.4.0 → subedit-0.5.0}/tests/repository/test_self_hosting.py +19 -2
  57. {subedit-0.4.0 → subedit-0.5.0}/uv.lock +167 -163
  58. subedit-0.4.0/docs/rules.md +0 -97
  59. subedit-0.4.0/src/subedit/domain/models/title_case.py +0 -81
  60. subedit-0.4.0/src/subedit/domain/rules/markdown.py +0 -173
  61. subedit-0.4.0/src/subedit/domain/rules/style.py +0 -229
  62. subedit-0.4.0/src/subedit/infrastructure/config_sources/config_schema.py +0 -45
  63. subedit-0.4.0/src/subedit/vale/styles/subedit/SignatureTag.yml +0 -7
  64. subedit-0.4.0/tests/domain/test_style.py +0 -236
  65. {subedit-0.4.0 → subedit-0.5.0}/.claude/settings.json +0 -0
  66. {subedit-0.4.0 → subedit-0.5.0}/.claude-plugin/marketplace.json +0 -0
  67. {subedit-0.4.0 → subedit-0.5.0}/.github/dependabot.yml +0 -0
  68. {subedit-0.4.0 → subedit-0.5.0}/.github/release.yml +0 -0
  69. {subedit-0.4.0 → subedit-0.5.0}/.github/workflows/ci.yml +0 -0
  70. {subedit-0.4.0 → subedit-0.5.0}/.github/workflows/pr.yml +0 -0
  71. {subedit-0.4.0 → subedit-0.5.0}/.github/workflows/release.yml +0 -0
  72. {subedit-0.4.0 → subedit-0.5.0}/.gitignore +0 -0
  73. {subedit-0.4.0 → subedit-0.5.0}/LICENSE +0 -0
  74. {subedit-0.4.0 → subedit-0.5.0}/docs/assets/cover.svg +0 -0
  75. {subedit-0.4.0 → subedit-0.5.0}/docs/assets/icon.svg +0 -0
  76. {subedit-0.4.0 → subedit-0.5.0}/docs/assets/layers.excalidraw +0 -0
  77. {subedit-0.4.0 → subedit-0.5.0}/docs/assets/layers.svg +0 -0
  78. {subedit-0.4.0 → subedit-0.5.0}/docs/assets/logo.svg +0 -0
  79. {subedit-0.4.0 → subedit-0.5.0}/docs/assets/pipeline.excalidraw +0 -0
  80. {subedit-0.4.0 → subedit-0.5.0}/docs/assets/pipeline.svg +0 -0
  81. {subedit-0.4.0 → subedit-0.5.0}/docs/assets/wordmark.svg +0 -0
  82. {subedit-0.4.0 → subedit-0.5.0}/plugins/subedit/hooks/hooks.json +0 -0
  83. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/application/__init__.py +0 -0
  84. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/application/fixer.py +0 -0
  85. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/application/linter.py +0 -0
  86. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/application/report_aggregator.py +0 -0
  87. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/models/comment_layout.py +0 -0
  88. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/models/document.py +0 -0
  89. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/models/embedded.py +0 -0
  90. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/models/file_type.py +0 -0
  91. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/models/position.py +0 -0
  92. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/models/prose_map.py +0 -0
  93. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/models/text.py +0 -0
  94. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/ports/__init__.py +0 -0
  95. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/ports/external_linter.py +0 -0
  96. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/ports/language_conventions.py +0 -0
  97. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/ports/rule.py +0 -0
  98. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/domain/ports/source_parser.py +0 -0
  99. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/config_sources/formatter_width.py +0 -0
  100. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/linters/__init__.py +0 -0
  101. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/linters/autocorrect_linter.py +0 -0
  102. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/linters/source_path.py +0 -0
  103. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/linters/vale_linter.py +0 -0
  104. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/parsers/__init__.py +0 -0
  105. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/parsers/composite_parser.py +0 -0
  106. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/parsers/languages/__init__.py +0 -0
  107. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/parsers/languages/code_line.py +0 -0
  108. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/parsers/languages/comment_only_profile.py +0 -0
  109. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/parsers/languages/language_profile.py +0 -0
  110. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/parsers/languages/python_profile.py +0 -0
  111. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/parsers/languages/typescript_profile.py +0 -0
  112. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/infrastructure/parsers/tree_sitter_parser.py +0 -0
  113. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/presentation/__init__.py +0 -0
  114. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/presentation/harnesses/__init__.py +0 -0
  115. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/presentation/harnesses/agent_harness.py +0 -0
  116. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/presentation/harnesses/claude_code_harness.py +0 -0
  117. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/presentation/harnesses/codex_harness.py +0 -0
  118. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/presentation/report.py +0 -0
  119. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/presentation/rule_reference.py +0 -0
  120. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/vale/styles/subedit/DateRef.yml +0 -0
  121. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/vale/styles/subedit/EditHistory.yml +0 -0
  122. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/vale/styles/subedit/EmDash.yml +0 -0
  123. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/vale/styles/subedit/ExternalRef.yml +0 -0
  124. {subedit-0.4.0 → subedit-0.5.0}/src/subedit/vale/styles/subedit/Semicolon.yml +0 -0
  125. {subedit-0.4.0 → subedit-0.5.0}/tests/application/test_report_aggregator.py +0 -0
  126. {subedit-0.4.0 → subedit-0.5.0}/tests/application/test_surface_ocp.py +0 -0
  127. {subedit-0.4.0 → subedit-0.5.0}/tests/conftest.py +0 -0
  128. {subedit-0.4.0 → subedit-0.5.0}/tests/domain/test_comment.py +0 -0
  129. {subedit-0.4.0 → subedit-0.5.0}/tests/domain/test_embedded.py +0 -0
  130. {subedit-0.4.0 → subedit-0.5.0}/tests/domain/test_position.py +0 -0
  131. {subedit-0.4.0 → subedit-0.5.0}/tests/domain/test_text.py +0 -0
  132. {subedit-0.4.0 → subedit-0.5.0}/tests/factories.py +0 -0
  133. {subedit-0.4.0 → subedit-0.5.0}/tests/fixtures/autocorrect/unspaced.py +0 -0
  134. {subedit-0.4.0 → subedit-0.5.0}/tests/fixtures/vale/clean.md +0 -0
  135. {subedit-0.4.0 → subedit-0.5.0}/tests/fixtures/vale/violations.py +0 -0
  136. {subedit-0.4.0 → subedit-0.5.0}/tests/infrastructure/test_data_formats.py +0 -0
  137. {subedit-0.4.0 → subedit-0.5.0}/tests/infrastructure/test_language_parity.py +0 -0
  138. {subedit-0.4.0 → subedit-0.5.0}/tests/infrastructure/test_parser.py +0 -0
  139. {subedit-0.4.0 → subedit-0.5.0}/tests/infrastructure/test_parser_ocp.py +0 -0
  140. {subedit-0.4.0 → subedit-0.5.0}/tests/infrastructure/test_parser_oracle.py +0 -0
  141. {subedit-0.4.0 → subedit-0.5.0}/tests/infrastructure/test_typescript.py +0 -0
  142. {subedit-0.4.0 → subedit-0.5.0}/tests/presentation/test_harnesses.py +0 -0
  143. {subedit-0.4.0 → subedit-0.5.0}/tests/repository/test_plugin.py +0 -0
@@ -9,7 +9,6 @@ BasedOnStyles = subedit
9
9
  # These guard a comment against going stale. A Markdown note may tell a history,
10
10
  # cite an issue, date a release, or explain a tag on purpose.
11
11
  subedit.EditHistory = NO
12
- subedit.SignatureTag = NO
13
12
  subedit.DateRef = NO
14
13
  subedit.ExternalRef = NO
15
14
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: subedit
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: A subeditor for AI agents, keeping every sentence they write to your house style.
5
5
  Project-URL: Homepage, https://github.com/mroops0111/subedit
6
6
  Project-URL: Repository, https://github.com/mroops0111/subedit
@@ -209,8 +209,8 @@ Judgment rules, such as a long block or a date in a comment, default to `warning
209
209
  Reflow works on sentences rather than lines, so the result reads the way the author would have broken it.
210
210
 
211
211
  - **One Line When It Fits**: a sentence that fits takes one line, and a wrapped one that would fit is joined.
212
- - **Clause Commas Only**: a longer sentence breaks only at a comma that ends a clause, never inside a list, brackets, or a code span.
213
- - **No Stubs**: no break leaves a line shorter than the `stub-fragment` floor, 24 characters by default.
212
+ - **Clause Commas Only**: a longer sentence breaks only at a comma that ends a clause, never inside brackets or a code span, and preferably not inside a list.
213
+ - **No Short Lines**: no break leaves a line shorter than the `line-too-short` floor, 24 characters by default.
214
214
  - **Verbatim Blocks**: code examples, docstring sections, and JSDoc tags are left as written.
215
215
  - **Markdown Paragraphs**: Markdown has no column limit, so a paragraph takes one source line, and a hand-wrapped one is joined.
216
216
 
@@ -284,12 +284,12 @@ Registering the hook with both agents is fine, since each payload identifies its
284
284
 
285
285
  ### Configuration
286
286
 
287
- Settings come from `subedit.toml`, or from `[tool.subedit]` in `pyproject.toml`, whichever sits closest above the file being checked. Without either, `check` and `fix` run with the defaults.
287
+ Settings come from `subedit.toml`, or from `[tool.subedit]` in `pyproject.toml`, whichever sits closest above the file being checked. Without either, `check` and `fix` run with the defaults. A config with a misspelled key, level, or option stops subedit with an error that names the file and the key, so a typo never leaves a project on defaults it did not choose.
288
288
 
289
289
  A rule takes one of two forms, depending on whether it has options to set.
290
290
 
291
291
  - **Level Only**: a string, `"error"`, `"warning"`, or `"off"`. Every rule takes this form, including the delegated Vale and AutoCorrect rules.
292
- - **Level and Options**: a `[rules.<name>]` table, holding `severity` and the rule's options. Leave out `severity` to keep the default level. Only `block-too-long` and `stub-fragment` have options, listed in [Rules](#rules).
292
+ - **Level and Options**: a `[rules.<name>]` table, holding `severity` and the rule's options. Leave out `severity` to keep the default level. The [rule reference](docs/rules.md) lists each rule's options with their defaults.
293
293
 
294
294
  ```toml
295
295
  [subedit]
@@ -301,18 +301,23 @@ mid-clause-break = "warning"
301
301
  file-header-comment = "off"
302
302
  "subedit.DateRef" = "off"
303
303
 
304
- [rules.block-too-long]
304
+ [rules.comment-too-long]
305
305
  severity = "error"
306
- line = 3
306
+ max-plain-lines = 3
307
307
 
308
- [rules.stub-fragment]
309
- minimum_characters = 30
308
+ [rules.line-too-short]
309
+ min-line-characters = 30
310
+
311
+ [rules.heading-title-case]
312
+ exceptions = ["npm"]
310
313
  ```
311
314
 
315
+ A name Title Case must leave as written, such as `npm`, goes under `exceptions`, which headings, table headers, and list labels all follow.
316
+
312
317
  In `pyproject.toml`, the same keys sit under `[tool.subedit]`, `[tool.subedit.rules]`, and `[tool.subedit.rules.<name>]`.
313
318
 
314
319
  <details>
315
- <summary><b>Settings</b>: the width, excluded paths, and code blocks</summary>
320
+ <summary><b>Settings</b>: the width, excluded paths, code blocks, and abbreviations</summary>
316
321
 
317
322
  <br>
318
323
 
@@ -321,6 +326,7 @@ In `pyproject.toml`, the same keys sit under `[tool.subedit]`, `[tool.subedit.ru
321
326
  | `line-width` | Integer | The formatter's width, else 120 | The column every comment line must fit, counting indent and delimiter |
322
327
  | `exclude` | List of globs | `[]` | Paths a directory walk skips, relative to the config file |
323
328
  | `code-blocks` | Boolean | `false` | Whether the comments in a Markdown code block are read, in a language subedit supports |
329
+ | `abbreviations` | List of strings | `[]` | Abbreviations whose period ends no sentence, each written with its period and its case, such as `"Dept."` |
324
330
 
325
331
  Left unset, `line-width` follows the formatter that would format the file, read from its own config.
326
332
 
@@ -333,7 +339,7 @@ Left unset, `line-width` follows the formatter that would format the file, read
333
339
 
334
340
  ### Rules
335
341
 
336
- `subedit` ships 18 native rules, a bundled Vale style of 6 rules, and AutoCorrect. Every rule is set by name under `[rules]` to `"error"`, `"warning"`, or `"off"`, and its fix kind follows [Fix Kinds](#fix-kinds).
342
+ `subedit` ships 21 native rules, a bundled Vale style of 5 rules, and AutoCorrect. Every rule is set by name under `[rules]` to `"error"`, `"warning"`, or `"off"`, and its fix kind follows [Fix Kinds](#fix-kinds).
337
343
 
338
344
  Comments and Markdown share one house style, and differ where their forms differ. A comment has a column limit and sits beside the code it explains, while Markdown has no limit and is built from paragraphs and headings.
339
345
 
@@ -361,7 +367,7 @@ A surface is a kind of file where prose lives. Every surface goes through the sa
361
367
  | Code comments and docstrings | Available | Layout, because a comment must fit a column limit |
362
368
  | Markdown | Available | One line per paragraph and the shape of headings, because Markdown has no column limit |
363
369
 
364
- Markdown is read from `.md` and `.markdown` files, leaving front matter, code, tables, and HTML blocks alone. With `code-blocks = true`, the comments in a fenced block are read too, when its info string names a language subedit supports, such as `python`, `ts`, or `yaml`. Each block is checked as a file of that language would be, by Vale and AutoCorrect as well, except for the rules on a whole file. A code block is often an example, so this stays off unless the project asks. Editor diagnostics over LSP are planned, so the same findings show as you type.
370
+ Markdown is read from `.md` and `.markdown` files, leaving front matter, code, and HTML blocks alone, and reading only the header of a table. With `code-blocks = true`, the comments in a fenced block are read too, when its info string names a language subedit supports, such as `python`, `ts`, or `yaml`. Each block is checked as a file of that language would be, by Vale and AutoCorrect as well, except for the rules on a whole file. A code block is often an example, so this stays off unless the project asks. Editor diagnostics over LSP are planned, so the same findings show as you type.
365
371
 
366
372
  #### Languages
367
373
 
@@ -391,4 +397,4 @@ Each language is a profile that knows its comment forms, its doc comment, and wh
391
397
 
392
398
  ## License
393
399
 
394
- [MIT](LICENSE)
400
+ [MIT](LICENSE). The English abbreviations that end no sentence come from the [Unicode CLDR](https://cldr.unicode.org/) under the Unicode License v3, whose notice ships with the [data file](src/subedit/domain/models/cldr_sentence_suppressions.txt).
@@ -171,8 +171,8 @@ Judgment rules, such as a long block or a date in a comment, default to `warning
171
171
  Reflow works on sentences rather than lines, so the result reads the way the author would have broken it.
172
172
 
173
173
  - **One Line When It Fits**: a sentence that fits takes one line, and a wrapped one that would fit is joined.
174
- - **Clause Commas Only**: a longer sentence breaks only at a comma that ends a clause, never inside a list, brackets, or a code span.
175
- - **No Stubs**: no break leaves a line shorter than the `stub-fragment` floor, 24 characters by default.
174
+ - **Clause Commas Only**: a longer sentence breaks only at a comma that ends a clause, never inside brackets or a code span, and preferably not inside a list.
175
+ - **No Short Lines**: no break leaves a line shorter than the `line-too-short` floor, 24 characters by default.
176
176
  - **Verbatim Blocks**: code examples, docstring sections, and JSDoc tags are left as written.
177
177
  - **Markdown Paragraphs**: Markdown has no column limit, so a paragraph takes one source line, and a hand-wrapped one is joined.
178
178
 
@@ -246,12 +246,12 @@ Registering the hook with both agents is fine, since each payload identifies its
246
246
 
247
247
  ### Configuration
248
248
 
249
- Settings come from `subedit.toml`, or from `[tool.subedit]` in `pyproject.toml`, whichever sits closest above the file being checked. Without either, `check` and `fix` run with the defaults.
249
+ Settings come from `subedit.toml`, or from `[tool.subedit]` in `pyproject.toml`, whichever sits closest above the file being checked. Without either, `check` and `fix` run with the defaults. A config with a misspelled key, level, or option stops subedit with an error that names the file and the key, so a typo never leaves a project on defaults it did not choose.
250
250
 
251
251
  A rule takes one of two forms, depending on whether it has options to set.
252
252
 
253
253
  - **Level Only**: a string, `"error"`, `"warning"`, or `"off"`. Every rule takes this form, including the delegated Vale and AutoCorrect rules.
254
- - **Level and Options**: a `[rules.<name>]` table, holding `severity` and the rule's options. Leave out `severity` to keep the default level. Only `block-too-long` and `stub-fragment` have options, listed in [Rules](#rules).
254
+ - **Level and Options**: a `[rules.<name>]` table, holding `severity` and the rule's options. Leave out `severity` to keep the default level. The [rule reference](docs/rules.md) lists each rule's options with their defaults.
255
255
 
256
256
  ```toml
257
257
  [subedit]
@@ -263,18 +263,23 @@ mid-clause-break = "warning"
263
263
  file-header-comment = "off"
264
264
  "subedit.DateRef" = "off"
265
265
 
266
- [rules.block-too-long]
266
+ [rules.comment-too-long]
267
267
  severity = "error"
268
- line = 3
268
+ max-plain-lines = 3
269
269
 
270
- [rules.stub-fragment]
271
- minimum_characters = 30
270
+ [rules.line-too-short]
271
+ min-line-characters = 30
272
+
273
+ [rules.heading-title-case]
274
+ exceptions = ["npm"]
272
275
  ```
273
276
 
277
+ A name Title Case must leave as written, such as `npm`, goes under `exceptions`, which headings, table headers, and list labels all follow.
278
+
274
279
  In `pyproject.toml`, the same keys sit under `[tool.subedit]`, `[tool.subedit.rules]`, and `[tool.subedit.rules.<name>]`.
275
280
 
276
281
  <details>
277
- <summary><b>Settings</b>: the width, excluded paths, and code blocks</summary>
282
+ <summary><b>Settings</b>: the width, excluded paths, code blocks, and abbreviations</summary>
278
283
 
279
284
  <br>
280
285
 
@@ -283,6 +288,7 @@ In `pyproject.toml`, the same keys sit under `[tool.subedit]`, `[tool.subedit.ru
283
288
  | `line-width` | Integer | The formatter's width, else 120 | The column every comment line must fit, counting indent and delimiter |
284
289
  | `exclude` | List of globs | `[]` | Paths a directory walk skips, relative to the config file |
285
290
  | `code-blocks` | Boolean | `false` | Whether the comments in a Markdown code block are read, in a language subedit supports |
291
+ | `abbreviations` | List of strings | `[]` | Abbreviations whose period ends no sentence, each written with its period and its case, such as `"Dept."` |
286
292
 
287
293
  Left unset, `line-width` follows the formatter that would format the file, read from its own config.
288
294
 
@@ -295,7 +301,7 @@ Left unset, `line-width` follows the formatter that would format the file, read
295
301
 
296
302
  ### Rules
297
303
 
298
- `subedit` ships 18 native rules, a bundled Vale style of 6 rules, and AutoCorrect. Every rule is set by name under `[rules]` to `"error"`, `"warning"`, or `"off"`, and its fix kind follows [Fix Kinds](#fix-kinds).
304
+ `subedit` ships 21 native rules, a bundled Vale style of 5 rules, and AutoCorrect. Every rule is set by name under `[rules]` to `"error"`, `"warning"`, or `"off"`, and its fix kind follows [Fix Kinds](#fix-kinds).
299
305
 
300
306
  Comments and Markdown share one house style, and differ where their forms differ. A comment has a column limit and sits beside the code it explains, while Markdown has no limit and is built from paragraphs and headings.
301
307
 
@@ -323,7 +329,7 @@ A surface is a kind of file where prose lives. Every surface goes through the sa
323
329
  | Code comments and docstrings | Available | Layout, because a comment must fit a column limit |
324
330
  | Markdown | Available | One line per paragraph and the shape of headings, because Markdown has no column limit |
325
331
 
326
- Markdown is read from `.md` and `.markdown` files, leaving front matter, code, tables, and HTML blocks alone. With `code-blocks = true`, the comments in a fenced block are read too, when its info string names a language subedit supports, such as `python`, `ts`, or `yaml`. Each block is checked as a file of that language would be, by Vale and AutoCorrect as well, except for the rules on a whole file. A code block is often an example, so this stays off unless the project asks. Editor diagnostics over LSP are planned, so the same findings show as you type.
332
+ Markdown is read from `.md` and `.markdown` files, leaving front matter, code, and HTML blocks alone, and reading only the header of a table. With `code-blocks = true`, the comments in a fenced block are read too, when its info string names a language subedit supports, such as `python`, `ts`, or `yaml`. Each block is checked as a file of that language would be, by Vale and AutoCorrect as well, except for the rules on a whole file. A code block is often an example, so this stays off unless the project asks. Editor diagnostics over LSP are planned, so the same findings show as you type.
327
333
 
328
334
  #### Languages
329
335
 
@@ -353,4 +359,4 @@ Each language is a profile that knows its comment forms, its doc comment, and wh
353
359
 
354
360
  ## License
355
361
 
356
- [MIT](LICENSE)
362
+ [MIT](LICENSE). The English abbreviations that end no sentence come from the [Unicode CLDR](https://cldr.unicode.org/) under the Unicode License v3, whose notice ships with the [data file](src/subedit/domain/models/cldr_sentence_suppressions.txt).
@@ -0,0 +1,105 @@
1
+ # Rules
2
+
3
+ `subedit` ships 21 native rules, a bundled Vale style of 5 rules, and AutoCorrect. The [README](../README.md#rules) compares what comments and Markdown share and where they differ, and this page lists every rule by group.
4
+
5
+ Every rule is set by name under `[rules]` to `"error"`, `"warning"`, or `"off"`, as in `"subedit.DateRef" = "off"`. An error fails `check` and stops an agent, while a warning is shown as advice. The fix kind states who resolves a finding, as [Fix Kinds](../README.md#fix-kinds) describes.
6
+
7
+ ## Style
8
+
9
+ These rules read comments and Markdown alike, and hold the punctuation, labels, and wording of the house style.
10
+
11
+ | Rule | Catches | Fix Kind | Default | Options |
12
+ | --- | --- | --- | --- | --- |
13
+ | `mid-sentence-colon` | A colon, half-width or full-width, joining clauses or leading into an inline list | Rewrite | `error` | `max-label-words` (5), `max-bold-label-words` (10), `max-full-width-label-characters` (12), `max-tag-words` (3) |
14
+ | `parenthesis-style` | Parentheses around prose in the width `style` does not pick, or spaced wrong for their width | Targeted | `error` | `style` (`half-width`) |
15
+ | `list-label-title-case` | A `label: description` item whose English label is not in Title Case | Targeted | `error` | None |
16
+ | `list-label-not-bold` | A `label: description` item whose label is not bold where the text renders | Targeted | `error` | None |
17
+ | `discouraged-word` | A word the project writes otherwise, replaced with the one it prefers | Targeted | `error` | `replacements` (1 pair) |
18
+ | `contrast-framing` | A Chinese sentence framed as *not A, but B*, which states B against what it is not | Rewrite | `warning` | None |
19
+
20
+ A colon may stand in a heading, after a short label, or at the end of a line that a list or block follows. A label opens a list item, or a paragraph as a capitalized tag such as `Note`, and carries no verb such as *is* or *can*. A label the author set in bold may run longer. Words opening a list item, or set in bold, that run past these limits may still be a label, so their colon is a warning unless the project sets the rule's severity. A colon anywhere else stays an error. Both list label rules read the same limits, so every rule agrees on what a label is. Parentheses around code, such as `foo(bar)`, are left alone. `half-width` parentheses take a space outside and none inside, the house style. `full-width` follows the CJK standard, where an aside in or beside CJK text takes full-width parentheses with no spaces, and one in English keeps half-width ones. By default `discouraged-word` replaces one Chinese verb for *include* with the one the house style prefers. A project lists its own pairs under `replacements`, as in `{ utilize = "use" }`, or sets it to `{}`. A word in Latin letters matches whole, and a CJK word wherever it stands. Bold goes only where a reader sees it rendered, in Markdown, docstrings, and JSDoc, not in a line comment. Quoted text, in a block quote or quotation marks, is left as written.
21
+
22
+ ## Wording
23
+
24
+ These rules come from the bundled Vale style, and the author rewrites what they find. They run when Vale is installed, and only in a repository without its own `.vale.ini`. A repository with one runs its own styles instead, and their findings join the same report.
25
+
26
+ | Rule | Catches | Fix Kind | Default | Options |
27
+ | --- | --- | --- | --- | --- |
28
+ | `subedit.EmDash` | An em dash, an en dash, or an arrow in prose | Rewrite | `error` | None |
29
+ | `subedit.Semicolon` | A semicolon, half-width or full-width, joining two clauses | Rewrite | `error` | None |
30
+ | `subedit.EditHistory` | A phrase such as "no longer" or "used to" that may narrate a change, in comments only | Rewrite | `warning` | None |
31
+ | `subedit.ExternalRef` | An identifier shaped like a ticket number a reader may not be able to open, in comments only | Rewrite | `warning` | None |
32
+ | `subedit.DateRef` | A date that records when something changed, in comments only | Rewrite | `warning` | None |
33
+
34
+ Standards such as `UTF-8` and `SHA-256` are exempt from `subedit.ExternalRef`. The `subedit:judging-findings` skill gives an agent the criteria for these judgment calls.
35
+
36
+ ## Spacing
37
+
38
+ AutoCorrect reads comments and Markdown alike, and autofixes the spacing between CJK and Latin text.
39
+
40
+ | Rule | Catches | Fix Kind | Default | Options |
41
+ | --- | --- | --- | --- | --- |
42
+ | `autocorrect.Spacing` | Missing spaces and wrong punctuation width between CJK and Latin text | Targeted | `error` | `.autocorrectrc` |
43
+
44
+ AutoCorrect ships as a Python dependency, so it runs wherever `subedit` does, and reads `.autocorrectrc` from the working directory.
45
+
46
+ ## Layout
47
+
48
+ These rules read comments, and autofix how a comment breaks across lines.
49
+
50
+ | Rule | Catches | Fix Kind | Default | Options |
51
+ | --- | --- | --- | --- | --- |
52
+ | `mid-clause-break` | A sentence broken where it need not be, or a wrapped sentence that would fit one line | Reflow | `error` | None |
53
+ | `line-too-long` | A comment line past the width, counting indent and delimiter | Reflow, or Rewrite for a trailing comment | `error` | None |
54
+
55
+ A trailing comment cannot break without leaving its code behind, so `line-too-long` asks the author to shorten it or move it above the code.
56
+
57
+ ## Sentence
58
+
59
+ These rules read comments, and catch the sentences no layout can save, which the author rewrites.
60
+
61
+ | Rule | Catches | Fix Kind | Default | Options |
62
+ | --- | --- | --- | --- | --- |
63
+ | `unbreakable-sentence` | A sentence over the width with no clause comma, or a summary that does not fit one line | Rewrite | `error` | None |
64
+ | `line-too-short` | A forced break that leaves a line too short to read as a clause | Rewrite | `warning` | `min-line-characters` (24) |
65
+ | `mid-list-break` | A break that seems to fall between the items of a list | Rewrite | `warning` | `max-item-words` (3) |
66
+
67
+ Reflow reads the same `min-line-characters` floor, so a fix never lays out a line that `line-too-short` then rejects.
68
+
69
+ Whether a comma parts list items, clauses, or an aside is a matter of syntax, which subedit can only guess, so `mid-list-break` is a warning. Reflow still prefers a break that splits no list when it lays a sentence out itself, but it keeps a legal break the author chose. A piece of up to `max-item-words` words between two commas may be a list item. Switching either rule off leaves its concern out of reflow too.
70
+
71
+ A period ends a sentence unless it closes an abbreviation. The abbreviations are the English ones Unicode CLDR lists, such as *Mr.* and *Ph.D.*, matched with their case, the Latin ones in lowercase, such as *e.g.* and *etc.*, and a reference such as *Fig.* or *Sec.* before a number. Many CLDR entries, such as *Go.* and *OK.*, are also words, so one ends no sentence only before a lowercase word or a number, unless it holds a period inside, as *U.S.* does. A title such as *Dr.* ends none before a name. A project adds its own under `abbreviations` in its settings, which never end a sentence.
72
+
73
+ ## Structure
74
+
75
+ These rules read comments, and check where documentation lives and how much of it there is.
76
+
77
+ | Rule | Catches | Fix Kind | Default | Options |
78
+ | --- | --- | --- | --- | --- |
79
+ | `plain-comment-on-public` | A public symbol documented with a plain comment instead of a docstring or JSDoc block | Rewrite | `error` | None |
80
+ | `signature-restating-tag` | A doc comment section that restates types the signature already carries, such as `Args:` or `@param` | Rewrite | `error` | None |
81
+ | `file-header-comment` | A banner comment opening the file before its first declaration | Rewrite | `warning` | None |
82
+ | `comment-too-long` | A comment long enough to restate the code below it | Rewrite | `warning` | `max-plain-lines` (4), `max-internal-doc-lines` (13), `max-public-doc-lines` (20) |
83
+
84
+ The `comment-too-long` limits are line counts, for a plain comment and for a doc comment on an internal or a public symbol, set from the 95th percentile of a measured corpus. `subedit stats` reports the same percentiles for your codebase.
85
+
86
+ A plain comment right under another declaration may head a group rather than document one symbol, so `plain-comment-on-public` reports that case as a warning.
87
+
88
+ ## Markdown
89
+
90
+ These rules read Markdown files, and keep each paragraph on one source line and each heading in its house form.
91
+
92
+ | Rule | Catches | Fix Kind | Default | Options |
93
+ | --- | --- | --- | --- | --- |
94
+ | `paragraph-line-break` | A paragraph or list item broken across source lines | Reflow | `error` | None |
95
+ | `heading-wrapped-in-bold` | A heading whose whole text is bold, though a heading already renders bold | Targeted | `error` | None |
96
+ | `heading-title-case` | An English heading not in Title Case | Targeted | `error` | `exceptions` (`[]`), `style` (`apa`), `levels` (`[1, 2, 3, 4, 5, 6]`) |
97
+ | `table-header-title-case` | An English table header cell not in Title Case | Targeted | `error` | None |
98
+ | `missing-lead-sentence` | A heading that opens straight into a list, with no sentence to frame it | Rewrite | `error` | None |
99
+ | `heading-not-noun-phrase` | A heading that asks a question or gives an instruction | Rewrite | `error` | `levels` (`[1, 2, 3, 4, 5, 6]`) |
100
+
101
+ GitHub issues, Notion, and Heptabase can show a source newline as a line break, so a hand-wrapped paragraph breaks once it is copied there. Joining puts no space between two CJK characters, and keeps a break the author marked with two trailing spaces or a backslash.
102
+
103
+ Title Case follows a published standard, which `style` names. `apa` lowercases articles, conjunctions, and prepositions of three letters or fewer, so it writes *Working With a List*. `chicago` lowercases articles, *and*, *but*, *for*, *or*, *nor*, and every preposition, so it writes *Working with a List*. A word such as *up* or *in* that may stand as an adverb, as in *Set Up*, keeps the case the author wrote, and a capitalized one inside a heading is a warning. Title Case leaves code, file names, acronyms, and a lone lowercase word alone. A lowercase name such as `npm` goes under `exceptions`. List labels and table headers follow `style` and `exceptions` too. A table is otherwise left unread, since its cells are fragments rather than sentences.
104
+
105
+ A heading that opens with a question word such as *How* or *What*, or ends with a question mark, is a question, so it is an error. A heading whose first word is followed straight by a word opening an object, such as *a*, *the*, or *your*, reads as an instruction, as in *Add a Rule*. That is a guess, so it is a warning unless the project sets the rule's severity. A single word such as *Install* passes, since it may name the subject. Both heading rules read only the heading levels listed under `levels`, so a project can leave out a document title or a FAQ.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "subedit",
3
3
  "displayName": "subedit",
4
- "version": "0.4.0",
4
+ "version": "0.5.0",
5
5
  "description": "Repairs prose layout silently on write, and returns only what needs rewriting.",
6
6
  "author": {
7
7
  "name": "mroops0111"
@@ -4,7 +4,7 @@
4
4
  # because the first run installs subedit and downloads Vale, which no edit should wait on.
5
5
  set -uo pipefail
6
6
 
7
- SPEC='subedit[vale]>=0.4.0'
7
+ SPEC='subedit[vale]>=0.5.0'
8
8
  # A bare uvx takes the first Python it finds, which on a stock Mac is too old for subedit.
9
9
  PYTHON='>=3.11'
10
10
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: judging-findings
3
- description: Decide what to do with a subedit finding that no tool can fix, such as subedit.ExternalRef, subedit.DateRef, mid-sentence-colon, block-too-long, or unbreakable-sentence. Use when a subedit hook, `subedit check`, or `subedit fix` reports a finding that needs rewriting or a warning that is a judgment call.
3
+ description: Decide what to do with a subedit finding that no tool can fix, such as subedit.ExternalRef, subedit.DateRef, mid-sentence-colon, comment-too-long, or unbreakable-sentence. Use when a subedit hook, `subedit check`, or `subedit fix` reports a finding that needs rewriting or a warning that is a judgment call.
4
4
  ---
5
5
 
6
6
  # Judge subedit Findings
@@ -23,25 +23,26 @@ Rewrite in place, keep the meaning, and keep the comment to the constraint the c
23
23
 
24
24
  A warning passes when the text meets its criterion. Leave such text exactly as it is, and say so in one line if anyone asks.
25
25
 
26
- | Rule | Keep the text when | Rewrite it when |
26
+ | Rule | Keep the Text When | Rewrite It When |
27
27
  | --- | --- | --- |
28
28
  | `subedit.ExternalRef` | The identifier is public and resolvable by anyone, such as an RFC, an MCP SEP, a PEP, a CVE, or an ISO standard. | It is a private ticket, such as a Jira key or an internal issue number. State the constraint the ticket recorded instead. |
29
29
  | `subedit.DateRef` | The date names a version or a spec revision, such as the dated revision an MCP specification is named by. | It records when something happened or changed. State the constraint that holds now. |
30
30
  | `subedit.EditHistory` | The phrase states a constraint that holds now, as in "a file no longer than 10 lines". | It tells how the code used to behave. State the behavior it has now. |
31
31
  | `contrast-framing` | Never in the house style, unless the contrast is the point, written as 是 B 而非 A. | It frames B against what it is not. State B directly. |
32
32
  | `heading-not-noun-phrase` | The heading is a noun phrase that happens to open with the flagged word. | It asks a question or gives an instruction. Name the section's subject instead. |
33
- | `block-too-long` | Every sentence states a constraint the code cannot show. | Some sentences narrate what the code does. Cut them, or move the explanation into a name. |
33
+ | `comment-too-long` | Every sentence states a constraint the code cannot show. | Some sentences narrate what the code does. Cut them, or move the explanation into a name. |
34
34
  | `file-header-comment` | It states a constraint about the whole module that no single declaration owns. | It is a banner or a summary of the file. Delete it, or move the constraint to the declaration it applies to. |
35
- | `stub-fragment` | Never. A stub reads as a broken clause. | Always. Rewrite the sentence to fit one line, or split it into two complete sentences. |
35
+ | `line-too-short` | Never. A line that short reads as a broken clause. | Always. Rewrite the sentence to fit one line, or split it into two complete sentences. |
36
+ | `mid-list-break` | The comma at the break parts an aside or a clause, as in "stays a reference, rewritten to X, and the component is expanded". | It parts the items of a list. Move the list to the end of the sentence, or split the sentence in two. |
36
37
 
37
38
  ## Errors
38
39
 
39
40
  An error always needs a rewrite. The message states the fix, and these notes cover what it leaves out.
40
41
 
41
- - **`unbreakable-sentence`**: a line can break only at a comma that ends a clause, and a comma inside brackets or a code span does not count. Add a comma at a clause boundary, or split the sentence in two. When every such comma would split a list, move the list to the end of the sentence. When the message names a style guide that keeps a summary to one line, shorten the summary and move the detail into the body.
42
+ - **`unbreakable-sentence`**: a line can break only at a comma that ends a clause, and a comma inside brackets or a code span does not count. Add a comma at a clause boundary, or split the sentence in two. When the message names a style guide that keeps a summary to one line, shorten the summary and move the detail into the body.
42
43
  - **`line-too-long`** (trailing comment): a comment beside code cannot break, so shorten it or move it to its own line above the code.
43
44
  - **`plain-comment-on-public`**: move the text into the doc comment, a docstring in Python or a `/** */` block in TypeScript.
44
- - **`signature-restating-tag` and `subedit.SignatureTag`**: delete the tag, since the signature already says it.
45
+ - **`signature-restating-tag`**: delete the tag, since the signature already says it.
45
46
  - **`mid-sentence-colon`**: a colon stands only in a heading, after a short label, or at the end of a line that a list or block follows. Split the sentence in two, or turn the inline list into a list.
46
47
  - **`missing-lead-sentence`**: add a sentence or two under the heading that frames what the list holds.
47
48
  - **`subedit.Semicolon`, `subedit.EmDash`**: split into two sentences, or use a comma or parentheses.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "subedit"
3
- version = "0.4.0"
3
+ version = "0.5.0"
4
4
  description = "A subeditor for AI agents, keeping every sentence they write to your house style."
5
5
  authors = [
6
6
  {name = "Yun-Tai Yang"},
@@ -1,3 +1,3 @@
1
- __version__ = '0.4.0'
1
+ __version__ = '0.5.0'
2
2
 
3
3
  __all__ = ['__version__']
@@ -86,8 +86,8 @@ class CorpusMeter:
86
86
  visibility = Visibility.PUBLIC if group.attaches_to_public_symbol else Visibility.INTERNAL
87
87
  return f'doc/{visibility.value}'
88
88
  if group.placement is CommentPlacement.FILE_HEADER:
89
- return 'line/file-header'
90
- return f'line/{group.placement.value}'
89
+ return 'plain/file-header'
90
+ return f'plain/{group.placement.value}'
91
91
 
92
92
  def _is_unbreakable(self, sentence: str, width: int) -> bool:
93
93
  return len(sentence) > width and ',' not in sentence
@@ -3,11 +3,13 @@ from .models import (
3
3
  PLAIN_MARKUP,
4
4
  QUOTED_DOC_LAYOUT,
5
5
  SEVERITY_BY_NAME,
6
+ ChoiceOption,
6
7
  CommentForm,
7
8
  CommentGroup,
8
9
  CommentLayout,
9
10
  CommentLine,
10
11
  CommentPlacement,
12
+ CountOption,
11
13
  Diagnostic,
12
14
  Document,
13
15
  EmbeddedDocument,
@@ -16,6 +18,7 @@ from .models import (
16
18
  Fix,
17
19
  FixKind,
18
20
  FramedDocLayout,
21
+ HeadingLevelsOption,
19
22
  LinePrefixLayout,
20
23
  MarkdownBlock,
21
24
  MarkdownBlockKind,
@@ -28,8 +31,10 @@ from .models import (
28
31
  ProseMarkup,
29
32
  ProseUnit,
30
33
  QuotedDocLayout,
34
+ ReplacementsOption,
31
35
  Report,
32
36
  RuleMetadata,
37
+ RuleOption,
33
38
  RuleSet,
34
39
  RuleSettings,
35
40
  Sentence,
@@ -39,6 +44,7 @@ from .models import (
39
44
  SymbolRef,
40
45
  TextEdit,
41
46
  Visibility,
47
+ WordsOption,
42
48
  apply_edits,
43
49
  )
44
50
  from .ports import (
@@ -57,11 +63,13 @@ __all__ = [
57
63
  'PLAIN_MARKUP',
58
64
  'QUOTED_DOC_LAYOUT',
59
65
  'SEVERITY_BY_NAME',
66
+ 'ChoiceOption',
60
67
  'CommentForm',
61
68
  'CommentGroup',
62
69
  'CommentLayout',
63
70
  'CommentLine',
64
71
  'CommentPlacement',
72
+ 'CountOption',
65
73
  'DelimiterSplit',
66
74
  'Diagnostic',
67
75
  'Document',
@@ -72,6 +80,7 @@ __all__ = [
72
80
  'Fix',
73
81
  'FixKind',
74
82
  'FramedDocLayout',
83
+ 'HeadingLevelsOption',
75
84
  'LanguageConventions',
76
85
  'LinePrefixLayout',
77
86
  'MarkdownBlock',
@@ -86,10 +95,12 @@ __all__ = [
86
95
  'ProseMarkup',
87
96
  'ProseUnit',
88
97
  'QuotedDocLayout',
98
+ 'ReplacementsOption',
89
99
  'Report',
90
100
  'Rule',
91
101
  'RuleContext',
92
102
  'RuleMetadata',
103
+ 'RuleOption',
93
104
  'RuleSet',
94
105
  'RuleSettings',
95
106
  'Sentence',
@@ -100,5 +111,6 @@ __all__ = [
100
111
  'SymbolRef',
101
112
  'TextEdit',
102
113
  'Visibility',
114
+ 'WordsOption',
103
115
  'apply_edits',
104
116
  ]
@@ -24,6 +24,7 @@ from .position import Encoding, Position, PositionMap, SourceRange
24
24
  from .prose import PLAIN_MARKUP, Prose, ProseMarkup, Sentence
25
25
  from .prose_map import ProseMap
26
26
  from .prose_unit import ProseLine, ProseUnit
27
+ from .rule_option import ChoiceOption, CountOption, HeadingLevelsOption, ReplacementsOption, RuleOption, WordsOption
27
28
  from .rule_set import RuleSet, RuleSettings
28
29
  from .text import TextEdit, apply_edits
29
30
 
@@ -33,11 +34,13 @@ __all__ = [
33
34
  'PLAIN_MARKUP',
34
35
  'QUOTED_DOC_LAYOUT',
35
36
  'SEVERITY_BY_NAME',
37
+ 'ChoiceOption',
36
38
  'CommentForm',
37
39
  'CommentGroup',
38
40
  'CommentLayout',
39
41
  'CommentLine',
40
42
  'CommentPlacement',
43
+ 'CountOption',
41
44
  'Diagnostic',
42
45
  'Document',
43
46
  'EmbeddedDocument',
@@ -46,6 +49,7 @@ __all__ = [
46
49
  'Fix',
47
50
  'FixKind',
48
51
  'FramedDocLayout',
52
+ 'HeadingLevelsOption',
49
53
  'LinePrefixLayout',
50
54
  'MarkdownBlock',
51
55
  'MarkdownBlockKind',
@@ -58,8 +62,10 @@ __all__ = [
58
62
  'ProseMarkup',
59
63
  'ProseUnit',
60
64
  'QuotedDocLayout',
65
+ 'ReplacementsOption',
61
66
  'Report',
62
67
  'RuleMetadata',
68
+ 'RuleOption',
63
69
  'RuleSet',
64
70
  'RuleSettings',
65
71
  'Sentence',
@@ -69,5 +75,6 @@ __all__ = [
69
75
  'SymbolRef',
70
76
  'TextEdit',
71
77
  'Visibility',
78
+ 'WordsOption',
72
79
  'apply_edits',
73
80
  ]
@@ -0,0 +1,64 @@
1
+ import importlib.resources
2
+
3
+
4
+ # The abbreviations Unicode CLDR lists for English, each with its final period and matched with its case.
5
+ # A lone letter such as A. is an initial, which prose reading judges by whether it stands as a word of its own.
6
+ CLDR_SUPPRESSIONS = frozenset(
7
+ line
8
+ for line in importlib.resources.files(__name__.rpartition('.')[0])
9
+ .joinpath('cldr_sentence_suppressions.txt')
10
+ .read_text(encoding='utf-8')
11
+ .splitlines()
12
+ if line and not line.startswith('#') and len(line) > 2
13
+ )
14
+
15
+ # Latin abbreviations, which CLDR lists only capitalized, such as E.g., while prose mostly writes them lowercase.
16
+ _LATIN = frozenset({'al.', 'approx.', 'ca.', 'cf.', 'e.g.', 'etc.', 'i.e.', 'p.m.', 'resp.', 'viz.', 'vs.'})
17
+ # Abbreviations that point into a document, such as Fig. 3, which end no sentence only before a number.
18
+ _BEFORE_A_NUMBER = frozenset(
19
+ {'art.', 'ch.', 'eq.', 'eqs.', 'fig.', 'figs.', 'no.', 'nos.', 'p.', 'para.', 'ref.', 'sec.', 'vol.'}
20
+ )
21
+
22
+
23
+ # Courtesy titles and ranks, which stand before a name, so the capitalized word after one is that name.
24
+ _TITLES = frozenset(
25
+ {
26
+ 'capt.',
27
+ 'col.',
28
+ 'dr.',
29
+ 'gen.',
30
+ 'gov.',
31
+ 'hon.',
32
+ 'lt.',
33
+ 'maj.',
34
+ 'mr.',
35
+ 'mrs.',
36
+ 'ms.',
37
+ 'mt.',
38
+ 'mx.',
39
+ 'prof.',
40
+ 'pvt.',
41
+ 'sen.',
42
+ 'sgt.',
43
+ 'st.',
44
+ }
45
+ )
46
+
47
+
48
+ def ends_no_sentence(word: str, following: str, added: frozenset[str] = frozenset()) -> bool:
49
+ """Whether a word written with its period, such as Dr., abbreviates rather than ends the sentence.
50
+
51
+ added holds the project's own abbreviations, each written with its period and matched with its case.
52
+ Many CLDR entries, such as Go. or OK., are also words that end a sentence,
53
+ so one ends no sentence only where its context shows an abbreviation, as Punkt reads the case of the next word.
54
+ """
55
+ lowered = word.lower()
56
+ next_character = following.lstrip()[:1]
57
+ if word in added or lowered in _LATIN or (lowered in _TITLES and word[:1].isupper()):
58
+ return True
59
+ if lowered in _BEFORE_A_NUMBER:
60
+ return next_character.isdigit()
61
+ if word in CLDR_SUPPRESSIONS:
62
+ # A period inside the word, as in U.S. or Ph.D., marks it an abbreviation whatever follows.
63
+ return '.' in word[:-1] or not next_character.isupper()
64
+ return False