diff --git a/CHANGELOG.md b/CHANGELOG.md index c386cc99..1237a64b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines. - - - +## v0.11.10 - 2026-10-11 +#### Bug Fixes +- (**sentence**) keep journal title abbreviations inside the sentence - (4904216) - *HaoZeke* +- default to native parsers, fix markdown sentence splits and binstall assets (#524) - (d57a91d) - *HaoZeke* +- keep filled labels readable in dark themes - (b71af34) - *HaoZeke* +- keep link hover off filled buttons - (15c336b) - *HaoZeke* +#### Documentation +- describe the release archives and journal title abbreviations - (12f8f41) - *HaoZeke* +- record the section padding as 6rem 0 - (c5fd704) - *HaoZeke* +- record the page radii and section padding - (bcc9722) - *HaoZeke* +- record the product and the shipping design - (1bacd33) - *HaoZeke* +#### Miscellaneous Chores +- turn on the design detector hook - (202286b) - *HaoZeke* +#### Style +- keep the scroll hint still when motion is reduced - (c9b079c) - *HaoZeke* +- lead the feature list with one proof - (872900e) - *HaoZeke* +- share one face across the TurtleTech sites - (7e39642) - *HaoZeke* + ## v0.11.9 - 2026-10-04 #### Bug Fixes - (**pandoc**) keep org file keywords and add no heading drawers - (18520c0) - *HaoZeke* diff --git a/Cargo.lock b/Cargo.lock index aa651559..b5b30038 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2221,7 +2221,7 @@ checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" [[package]] name = "snapper-fmt" -version = "0.11.9" +version = "0.11.10" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 15f9151f..5aefbd96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "snapper-fmt" -version = "0.11.9" +version = "0.11.10" edition = "2024" autotests = false rust-version = "1.85" diff --git a/README.md b/README.md index c4b89d05..003b574f 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ It is not [admk/sembr](https://github.com/admk/sembr) (learned clause breaks) an Math environments, tables, front matter, drawers, and non-source comments pass through unchanged. Fenced and delimited source blocks are `Region::Code`: fence/open/close lines stay structure, non-comment code stays verbatim, and comment lines reflow at sentence boundaries when the language has a `[code.]` entry in `.snapperrc.toml` (`snapper init` seeds common languages). Pass `--format-code` to also pipe each block body through an optional per-language `formatter` argv (missing binary, non-zero exit, and timeouts degrade to the reflowed body). -Sentence detection relies on Unicode UAX #29 segmentation with abbreviation-aware post-processing that avoids false breaks at titles (Dr., Prof.), references (Fig., Eq.), and Latin terms (e.g., i.e., et al.). +Sentence detection relies on Unicode UAX #29 segmentation with abbreviation-aware post-processing that avoids false breaks at titles (Dr., Prof.), references (Fig., Eq.), journal titles (J. Chem. Phys., SIAM J. Sci. Comput.), and Latin terms (e.g., i.e., et al.). Org emphasis (`*bold*`, `/italic/`, `_underline_`, `+strike+`) is kept atomic so splits cannot open a pseudo-headline mid-span. Markdown `*em*` / `**strong**` (CommonMark flanking) and GFM `~~strike~~` are kept atomic the same way. @@ -75,6 +75,10 @@ Pre-built binary (fastest): cargo binstall snapper-fmt +`cargo binstall` fetches the archive for your platform from the [GitHub release](https://github.com/TurtleTech-ehf/snapper/releases/latest) and never compiles. +Each release ships archives for Linux on x86_64 and aarch64 (glibc, and static musl for Alpine and minimal containers), macOS on x86_64 and Apple silicon, and Windows on x86_64 (a zip). +The static musl builds cannot load the optional in-process pandoc library, so `--use-pandoc` there runs the `pandoc` on `PATH`. + Shell one-liner (Linux/macOS): curl -LsSf https://github.com/TurtleTech-ehf/snapper/releases/latest/download/snapper-fmt-installer.sh | sh @@ -243,7 +247,7 @@ Configuration guide (org source in-tree): `docs/orgmode/howto/mcp-integration.or ## Pre-commit hook - repo: https://github.com/TurtleTech-ehf/snapper - rev: v0.11.9 + rev: v0.11.10 hooks: - id: snapper diff --git a/conda/recipe.yaml b/conda/recipe.yaml index f79e0987..2d89af95 100644 --- a/conda/recipe.yaml +++ b/conda/recipe.yaml @@ -3,7 +3,7 @@ schema_version: 1 context: - version: "0.11.9" + version: "0.11.10" package: name: snapper-fmt diff --git a/docs/orgmode/howto/ci-enforcement.org b/docs/orgmode/howto/ci-enforcement.org index 3f01ac38..a8730095 100644 --- a/docs/orgmode/howto/ci-enforcement.org +++ b/docs/orgmode/howto/ci-enforcement.org @@ -10,7 +10,7 @@ The easiest way to enforce semantic line breaks in CI. Add to your workflow: #+begin_src yaml -- uses: TurtleTech-ehf/snapper@v0.11.9 +- uses: TurtleTech-ehf/snapper@v0.11.10 with: files: '**/*.org **/*.tex **/*.md' #+end_src @@ -20,7 +20,7 @@ This installs snapper and runs =--check= on the specified files. For GitHub Code Scanning integration (SARIF annotations on PRs): #+begin_src yaml -- uses: TurtleTech-ehf/snapper@v0.11.9 +- uses: TurtleTech-ehf/snapper@v0.11.10 with: files: '**/*.org **/*.tex **/*.md' sarif: 'true' @@ -35,7 +35,7 @@ Add to =.pre-commit-config.yaml=: #+begin_src yaml - repo: https://github.com/TurtleTech-ehf/snapper - rev: v0.11.9 + rev: v0.11.10 hooks: - id: snapper #+end_src diff --git a/docs/orgmode/howto/vale-integration.org b/docs/orgmode/howto/vale-integration.org index 37e83e98..f31edf81 100644 --- a/docs/orgmode/howto/vale-integration.org +++ b/docs/orgmode/howto/vale-integration.org @@ -70,7 +70,7 @@ A typical workflow: #+begin_src yaml - repo: https://github.com/TurtleTech-ehf/snapper - rev: v0.11.9 + rev: v0.11.10 hooks: - id: snapper - repo: https://github.com/errata-ai/vale diff --git a/docs/orgmode/reference/abbreviations.org b/docs/orgmode/reference/abbreviations.org index 31c170be..7691a0f1 100644 --- a/docs/orgmode/reference/abbreviations.org +++ b/docs/orgmode/reference/abbreviations.org @@ -67,6 +67,21 @@ vs., misc., est., govt., dept., univ., inc., corp., ltd., Ave., Blvd., Rd., pp., A. through Z. (for names like J. K. Rowling). +* Journal titles (every language) +:PROPERTIES: +:CUSTOM_ID: journals +:END: + +Reference lists print journal titles in ISO 4 form, as a run of abbreviated words (=J. Chem. Phys.=, =SIAM J. Sci. Comput.=, =ACM Trans. Math. Softw.=). +ISO 4 abbreviates the same words whatever the language, so this list applies under every =--lang=: + +Acad., Adv., Am., Anal., Angew., Annu., Appl., Arch., Astron., Astrophys., Biochem., Biol., Biophys., Bull., Catal., Chem., Chim., Clin., Comb., Commun., Comp., Comput., Crystallogr., Curr., Electrochem., Electron., Eng., Environ., Eur., Exp., Geophys., Ind., Inf., Inorg., Instrum., Int., Jpn., Lett., Lond., Macromol., Mater., Math., Mech., Med., Microsc., Mol., Nanotechnol., Natl., Nucl., Numer., Opt., Optim., Philos., Photochem., Phys., Polym., Proc., Psychol., Rep., Res., Sci., Ser., Simul., Soc., Softw., Spectrosc., Stat., Struct., Surf., Syst., Technol., Theor., Thermodyn. + +Words that also end ordinary sentences (=Front.=, =Org.=, =Ann.=) are left out. +A run still holds across them: a capitalised word and its period stay joined to the next piece when that piece opens with a listed journal word, so =Front. Chem. 8= and =J. Org. Chem.= stay on one line. +A title or a figure reference after the period still opens a sentence, so =We used Python. Fig. 3 shows= splits. +Add a missing journal word with =extra_abbreviations=. + * German abbreviations (=--lang de=) :PROPERTIES: :CUSTOM_ID: german diff --git a/docs/orgmode/tutorials/quickstart.org b/docs/orgmode/tutorials/quickstart.org index 3fbac9cd..bd50fe3d 100644 --- a/docs/orgmode/tutorials/quickstart.org +++ b/docs/orgmode/tutorials/quickstart.org @@ -16,6 +16,10 @@ Pre-built binary (fastest, no compilation): cargo binstall snapper-fmt #+end_src +=cargo binstall= fetches the archive for your platform from the [[https://github.com/TurtleTech-ehf/snapper/releases/latest][GitHub release]]. +Releases cover Linux on x86_64 and aarch64 (glibc and static musl), macOS on x86_64 and Apple silicon, and Windows on x86_64. +Pick the musl archive for Alpine or a minimal container. + Or compile from source: #+begin_src bash @@ -233,7 +237,7 @@ Add to your =.pre-commit-config.yaml=: #+begin_src yaml - repo: https://github.com/TurtleTech-ehf/snapper - rev: v0.11.9 + rev: v0.11.10 hooks: - id: snapper #+end_src diff --git a/docs/source/conf.py b/docs/source/conf.py index b6f9ab8a..7786b7b0 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -3,7 +3,7 @@ project = "snapper" copyright = '2026--present, Rohit Goswami' author = "Rohit Goswami" -release = "0.11.9" +release = "0.11.10" html_logo = "../../branding/logo/snapper_logo.png" extensions = [ diff --git a/editors/obsidian/manifest.json b/editors/obsidian/manifest.json index 7b081bb5..b4bca962 100644 --- a/editors/obsidian/manifest.json +++ b/editors/obsidian/manifest.json @@ -1,7 +1,7 @@ { "id": "snapper", "name": "Snapper - Semantic Line Breaks", - "version": "0.11.9", + "version": "0.11.10", "minAppVersion": "1.0.0", "description": "Format prose with semantic line breaks for clean git diffs. Supports Org-mode, LaTeX, Markdown, and plaintext.", "author": "TurtleTech", diff --git a/editors/obsidian/package-lock.json b/editors/obsidian/package-lock.json index 2ca8e011..830a260c 100644 --- a/editors/obsidian/package-lock.json +++ b/editors/obsidian/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-snapper", - "version": "0.11.9", + "version": "0.11.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-snapper", - "version": "0.11.9", + "version": "0.11.10", "license": "MIT", "devDependencies": { "@snapper/wasm": "file:../../packages/snapper-wasm", @@ -17,7 +17,7 @@ }, "../../packages/snapper-wasm": { "name": "@snapper/wasm", - "version": "0.11.9", + "version": "0.11.10", "dev": true, "license": "MIT", "devDependencies": { diff --git a/editors/obsidian/package.json b/editors/obsidian/package.json index c79be731..06e25de1 100644 --- a/editors/obsidian/package.json +++ b/editors/obsidian/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-snapper", - "version": "0.11.9", + "version": "0.11.10", "private": true, "description": "Obsidian plugin for snapper semantic line break formatter", "scripts": { diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 96122848..67553923 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -1,9 +1,9 @@ # snapper - Semantic Line Breaks -Requires the **snapper** / **snapper-fmt** CLI **0.11.9+** on your PATH (or set `snapper.path`). +Requires the **snapper** / **snapper-fmt** CLI **0.11.10+** on your PATH (or set `snapper.path`). The crate is `snapper-fmt`; installers ship both names. If [openSUSE snapper](https://github.com/openSUSE/snapper) already owns `/usr/bin/snapper`, call `snapper-fmt` or set `snapper.path`. -Install: `cargo install snapper-fmt` or the [release installer](https://github.com/TurtleTech-ehf/snapper/releases/tag/v0.11.9). +Install: `cargo install snapper-fmt` or the [release installer](https://github.com/TurtleTech-ehf/snapper/releases/tag/v0.11.10). Format prose so each sentence occupies its own line, producing clean git diffs for collaborative writing. diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 8c53f765..5dcd37d5 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -2,7 +2,7 @@ "name": "snapper", "displayName": "snapper - Semantic Line Breaks", "description": "Format prose with semantic line breaks for clean git diffs", - "version": "0.11.9", + "version": "0.11.10", "publisher": "TurtleTech", "license": "MIT", "repository": { diff --git a/editors/word/README.md b/editors/word/README.md index 621adac0..bf96bdd0 100644 --- a/editors/word/README.md +++ b/editors/word/README.md @@ -2,7 +2,7 @@ Office add-in that formats document prose with **semantic line breaks** (one sentence per line) using the **snapper WASM** build (`@snapper/wasm`). Useful when drafting in Word and exporting to Org, Markdown, or LaTeX for git-friendly diffs. -**Version:** 0.11.9 (tracks snapper-fmt) +**Version:** 0.11.10 (tracks snapper-fmt) Development preview; not published in AppSource. @@ -34,7 +34,7 @@ CI builds the Word add-in in `.github/workflows/wasm.yml` (`build-word` job). ## Relationship to the CLI -Word uses the **plaintext** format path in WASM. For Org/LaTeX fidelity, prefer the CLI or VS Code extension (LSP). Delimiter-span and abbreviation behavior matches the current WASM API (requires snapper **0.11.9+**). +Word uses the **plaintext** format path in WASM. For Org/LaTeX fidelity, prefer the CLI or VS Code extension (LSP). Delimiter-span and abbreviation behavior matches the current WASM API (requires snapper **0.11.10+**). ## License diff --git a/editors/word/manifest.xml b/editors/word/manifest.xml index 70797bf7..0f4714f5 100644 --- a/editors/word/manifest.xml +++ b/editors/word/manifest.xml @@ -5,7 +5,7 @@ xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xsi:type="TaskPaneApp"> a8f3c2e1-9b4d-4e7a-8c5f-1d2e3f4a5b6c - 0.11.9 + 0.11.10 TurtleTech ehf en-US diff --git a/editors/word/package-lock.json b/editors/word/package-lock.json index befa2cd6..124c6ec2 100644 --- a/editors/word/package-lock.json +++ b/editors/word/package-lock.json @@ -1,12 +1,12 @@ { "name": "word-snapper", - "version": "0.11.9", + "version": "0.11.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "word-snapper", - "version": "0.11.9", + "version": "0.11.10", "license": "MIT", "devDependencies": { "@snapper/wasm": "file:../../packages/snapper-wasm", @@ -21,7 +21,7 @@ }, "../../packages/snapper-wasm": { "name": "@snapper/wasm", - "version": "0.11.9", + "version": "0.11.10", "dev": true, "license": "MIT", "devDependencies": { diff --git a/editors/word/package.json b/editors/word/package.json index 8fb4af81..92b5da96 100644 --- a/editors/word/package.json +++ b/editors/word/package.json @@ -1,6 +1,6 @@ { "name": "word-snapper", - "version": "0.11.9", + "version": "0.11.10", "private": true, "description": "Microsoft Word add-in for snapper semantic line break formatter", "scripts": { diff --git a/editors/word/src/taskpane/taskpane.html b/editors/word/src/taskpane/taskpane.html index 9fcd3d9c..ebbdd490 100644 --- a/editors/word/src/taskpane/taskpane.html +++ b/editors/word/src/taskpane/taskpane.html @@ -10,7 +10,7 @@

Snapper

-

Semantic line breaks for Word (v0.11.9)

+

Semantic line breaks for Word (v0.11.10)

Formats paragraphs as plaintext. Use the CLI or VS Code for Org/LaTeX structure awareness.

diff --git a/npm/package.json b/npm/package.json index 2a733ae3..f7cbffc1 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "@turtletech/snapper-mcp", - "version": "0.11.9", + "version": "0.11.10", "private": true, "description": "MCP server for snapper semantic line break formatter", "main": "bin/run.js", diff --git a/packages/snapper-wasm/package-lock.json b/packages/snapper-wasm/package-lock.json index 81f37f91..a4010498 100644 --- a/packages/snapper-wasm/package-lock.json +++ b/packages/snapper-wasm/package-lock.json @@ -1,12 +1,12 @@ { "name": "@snapper/wasm", - "version": "0.11.9", + "version": "0.11.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@snapper/wasm", - "version": "0.11.9", + "version": "0.11.10", "license": "MIT", "devDependencies": { "typescript": "^5.4" diff --git a/packages/snapper-wasm/package.json b/packages/snapper-wasm/package.json index 9ab07005..05c91d7b 100644 --- a/packages/snapper-wasm/package.json +++ b/packages/snapper-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@snapper/wasm", - "version": "0.11.9", + "version": "0.11.10", "description": "WebAssembly build of snapper semantic line break formatter", "type": "module", "main": "dist/index.js", diff --git a/readme_src.org b/readme_src.org index e39d873b..7ba11e1d 100644 --- a/readme_src.org +++ b/readme_src.org @@ -31,7 +31,7 @@ It is not [[https://github.com/admk/sembr][admk/sembr]] (learned clause breaks) Math environments, tables, front matter, drawers, and non-source comments pass through unchanged. Fenced and delimited source blocks are =Region::Code=: fence/open/close lines stay structure, non-comment code stays verbatim, and comment lines reflow at sentence boundaries when the language has a =[code.]= entry in =.snapperrc.toml= (=snapper init= seeds common languages). Pass =--format-code= to also pipe each block body through an optional per-language =formatter= argv (missing binary, non-zero exit, and timeouts degrade to the reflowed body). -Sentence detection relies on Unicode UAX #29 segmentation with abbreviation-aware post-processing that avoids false breaks at titles (Dr., Prof.), references (Fig., Eq.), and Latin terms (e.g., i.e., et al.). +Sentence detection relies on Unicode UAX #29 segmentation with abbreviation-aware post-processing that avoids false breaks at titles (Dr., Prof.), references (Fig., Eq.), journal titles (J. Chem. Phys., SIAM J. Sci. Comput.), and Latin terms (e.g., i.e., et al.). Org emphasis (=*bold*=, =/italic/=, =_underline_=, =+strike+=) is kept atomic so splits cannot open a pseudo-headline mid-span. Markdown =*em*= / =**strong**= (CommonMark flanking) and GFM =~~strike~~= are kept atomic the same way. * Installation @@ -42,6 +42,9 @@ Pre-built binary (fastest): #+begin_src bash cargo binstall snapper-fmt #+end_src +=cargo binstall= fetches the archive for your platform from the [[https://github.com/TurtleTech-ehf/snapper/releases/latest][GitHub release]] and never compiles. +Each release ships archives for Linux on x86_64 and aarch64 (glibc, and static musl for Alpine and minimal containers), macOS on x86_64 and Apple silicon, and Windows on x86_64 (a zip). +The static musl builds cannot load the optional in-process pandoc library, so =--use-pandoc= there runs the =pandoc= on =PATH=. Shell one-liner (Linux/macOS): #+begin_src bash curl -LsSf https://github.com/TurtleTech-ehf/snapper/releases/latest/download/snapper-fmt-installer.sh | sh @@ -163,7 +166,7 @@ Configuration guide (org source in-tree): =docs/orgmode/howto/mcp-integration.or :END: #+begin_src yaml - repo: https://github.com/TurtleTech-ehf/snapper - rev: v0.11.9 + rev: v0.11.10 hooks: - id: snapper #+end_src diff --git a/site-svelte/src/app.html b/site-svelte/src/app.html index d0dc8793..fc66a3c2 100644 --- a/site-svelte/src/app.html +++ b/site-svelte/src/app.html @@ -31,7 +31,7 @@ "operatingSystem": "Linux, macOS, Windows", "url": "https://snapper.turtletech.us", "downloadUrl": "https://crates.io/crates/snapper-fmt", - "softwareVersion": "0.11.9", + "softwareVersion": "0.11.10", "license": "https://opensource.org/licenses/MIT", "author": { "@type": "Organization", "name": "TurtleTech ehf", "url": "https://turtletech.us" }, "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" } diff --git a/site-svelte/src/routes/+page.svelte b/site-svelte/src/routes/+page.svelte index feaacc0a..3f1eb533 100644 --- a/site-svelte/src/routes/+page.svelte +++ b/site-svelte/src/routes/+page.svelte @@ -73,7 +73,7 @@ lines: [ { text: '# .pre-commit-config.yaml', type: 'comment' as const }, { text: '- repo: https://github.com/TurtleTech-ehf/snapper', type: 'flag' as const }, - { text: ' rev: v0.11.9', type: 'flag' as const }, + { text: ' rev: v0.11.10', type: 'flag' as const }, { text: ' hooks:', type: 'flag' as const }, { text: ' - id: snapper', type: 'flag' as const }, ], diff --git a/site/index.html b/site/index.html index 71012a8b..dec61a9f 100644 --- a/site/index.html +++ b/site/index.html @@ -31,7 +31,7 @@ "operatingSystem": "Linux, macOS, Windows", "url": "https://snapper.turtletech.us", "downloadUrl": "https://crates.io/crates/snapper-fmt", - "softwareVersion": "0.11.9", + "softwareVersion": "0.11.10", "license": "https://opensource.org/licenses/MIT", "author": { "@type": "Organization", @@ -909,7 +909,7 @@

Install in seconds

Pre-commit
# .pre-commit-config.yaml
 - repo: https://github.com/TurtleTech-ehf/snapper
-  rev: v0.11.9
+  rev: v0.11.10
   hooks:
     - id: snapper
diff --git a/src/abbreviations.rs b/src/abbreviations.rs index 4da8c48a..4bced861 100644 --- a/src/abbreviations.rs +++ b/src/abbreviations.rs @@ -65,6 +65,25 @@ pub static PL_ABBREVIATIONS: &[&str] = &[ "T", "U", "V", "W", "X", "Y", "Z", ]; +/// Journal title abbreviations in ISO 4 form, as reference lists print them +/// (`J. Chem. Phys.`, `SIAM J. Sci. Comput.`, `ACM Trans. Math. Softw.`). +/// ISO 4 abbreviates the same words in every language, so these join a +/// sentence whatever `--lang` says. +/// Each entry is capitalised and is not an English word that ends a +/// sentence on its own, so `Org` and `Ann` stay out; a run such as +/// `J. Org. Chem.` or `Ann. Phys.` still holds through the next listed word. +#[rustfmt::skip] +pub static JOURNAL_ABBREVIATIONS: &[&str] = &[ + "Acad", "Adv", "Am", "Anal", "Angew", "Annu", "Appl", "Arch", "Astron", "Astrophys", + "Biochem", "Biol", "Biophys", "Bull", "Catal", "Chem", "Chim", "Clin", "Comb", "Commun", + "Comp", "Comput", "Crystallogr", "Curr", "Electrochem", "Electron", "Eng", "Environ", "Eur", + "Exp", "Geophys", "Ind", "Inf", "Inorg", "Instrum", "Int", "Jpn", "Lett", "Lond", "Macromol", + "Mater", "Math", "Mech", "Med", "Microsc", "Mol", "Nanotechnol", "Natl", "Nucl", "Numer", + "Opt", "Optim", "Philos", "Photochem", "Phys", "Polym", "Proc", "Psychol", "Rep", "Res", + "Sci", "Ser", "Simul", "Soc", "Softw", "Spectrosc", "Stat", "Struct", "Surf", "Syst", + "Technol", "Theor", "Thermodyn", +]; + /// Multi-word abbreviations where the period is after the second part. pub static EN_MULTI_ABBREVS: &[&str] = &["e.g", "i.e", "a.m", "p.m", "v.s", "w.r.t"]; pub static DE_MULTI_ABBREVS: &[&str] = &["z.B", "d.h", "u.a", "o.g", "s.o", "u.U"]; diff --git a/src/sentence/unicode.rs b/src/sentence/unicode.rs index 534ff1dc..8ad73d69 100644 --- a/src/sentence/unicode.rs +++ b/src/sentence/unicode.rs @@ -141,7 +141,12 @@ impl UnicodeSentenceSplitter { let abbrevs = abbreviations::abbreviations_for_lang(lang); let multi = abbreviations::multi_abbrevs_for_lang(lang); - let alts: Vec<&str> = abbrevs.to_vec(); + let mut alts: Vec<&str> = abbrevs.to_vec(); + alts.extend( + abbreviations::JOURNAL_ABBREVIATIONS + .iter() + .filter(|j| !abbrevs.contains(j)), + ); let pattern = format!(r#"(?:^|[\s"'`(\[])(?:{})$"#, alts.join("|")); let lang_abbrev_pattern = Regex::new(&pattern).expect("valid abbreviation regex"); @@ -3384,6 +3389,7 @@ fn merge_abbreviation_splits( for &segment in segments { let should_merge = if let Some(prev) = result.last() { is_abbreviation_ending(prev, abbrev_re, multi_re, extra) + || continues_journal_abbreviation_run(prev, segment) } else { false }; @@ -4128,6 +4134,26 @@ pub fn newlines_respect_delimiter_spans(formatted: &str) -> bool { true } +/// A citation prints a journal as a run of capitalised abbreviations +/// (`Front. Chem. 8`, `Ann. Phys.`, `J. Org. Chem.`). +/// The list holds the common ISO 4 words, but a run can open on a word it +/// leaves out because the word also ends sentences (`Front`, `Org`). +/// Hold the break after a capitalised word and its period when the next +/// piece opens on a listed journal word with its own period. +/// A title such as `Dr.` or `Fig.` opening the next piece is not a journal +/// word, so `We used Python. Fig. 3 shows` still splits. +fn continues_journal_abbreviation_run(prev: &str, next: &str) -> bool { + static PREV_RE: LazyLock = LazyLock::new(|| { + Regex::new(r"(?:^|[\s(\[])\p{Lu}\p{Ll}{0,11}\.$").expect("valid journal-run tail regex") + }); + static NEXT_RE: LazyLock = LazyLock::new(|| { + let alts = abbreviations::JOURNAL_ABBREVIATIONS.join("|"); + Regex::new(&format!(r"^(?:{alts})\.(?:\s|$|[,;:)\]])")) + .expect("valid journal-run head regex") + }); + PREV_RE.is_match(prev.trim_end()) && NEXT_RE.is_match(next.trim_start()) +} + fn is_abbreviation_ending( s: &str, abbrev_re: &Regex, @@ -4183,6 +4209,69 @@ mod tests { ); } + #[test] + fn journal_abbreviations_hold_the_sentence() { + for (journal, rest) in [ + ("J. Chem. Phys.", "150, 154105 (2019)"), + ("Front. Chem.", "8, 589 (2020)"), + ("J. Chem. Theory Comput.", "17, 3 (2021)"), + ("Modelling Simul. Mater. Sci. Eng.", "12, 3 (2004)"), + ("SIAM J. Sci. Comput.", "4, 5 (1983)"), + ("ACM Trans. Math. Softw.", "1, 2 (1975)"), + ("SIAM J. Optim.", "3, 1 (1993)"), + ("J. Am. Chem. Soc.", "141, 2 (2019)"), + ("Phys. Rev. Lett.", "77, 3865 (1996)"), + ] { + let text = format!("See Smith, {journal} {rest} for details. The next one opens."); + assert_eq!( + split(&text), + vec![ + format!("See Smith, {journal} {rest} for details."), + "The next one opens.".to_string(), + ], + "{journal} must not end a sentence" + ); + } + } + + #[test] + fn journal_abbreviation_runs_hold_through_unlisted_words() { + // `Org` and `Ann` end sentences on their own and are not listed; + // the listed word after them keeps the run whole. + assert_eq!( + split("It appeared in J. Org. Chem. 84, 1 and Ann. Phys. 4, 2. Done."), + vec![ + "It appeared in J. Org. Chem. 84, 1 and Ann. Phys. 4, 2.", + "Done." + ] + ); + } + + #[test] + fn journal_runs_leave_ordinary_breaks_alone() { + assert_eq!( + split("We used Python. Fig. 3 shows the rest."), + vec!["We used Python.", "Fig. 3 shows the rest."] + ); + assert_eq!( + split("The mode is written in Org. It reflows."), + vec!["The mode is written in Org.", "It reflows."] + ); + assert_eq!( + split("I met Ann. She waved."), + vec!["I met Ann.", "She waved."] + ); + } + + #[test] + fn journal_abbreviations_hold_in_every_language() { + let de = UnicodeSentenceSplitter::for_lang("de", &[]); + assert_eq!( + de.split("Siehe J. Chem. Phys. 150, 1 (2019). Danach mehr."), + vec!["Siehe J. Chem. Phys. 150, 1 (2019).", "Danach mehr."] + ); + } + #[test] fn a_number_opens_a_sentence() { assert_eq!( diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs index 66d719c4..f9a74f62 100644 --- a/tests/cli_integration.rs +++ b/tests/cli_integration.rs @@ -742,3 +742,50 @@ fn markdown_setext_heading_not_collapsed() { "must not glue underline onto title:\n{result}" ); } + +#[test] +fn reference_list_journal_abbreviations_stay_on_one_line() { + let input = "1. R. Goswami, J. Chem. Phys. 150, 154105 (2019). Next entry.\n\ + 2. A. B. Author, SIAM J. Sci. Comput. 4, 5 (1983).\n\ + 3. C. D. Author, ACM Trans. Math. Softw. 1, 2 (1975).\n"; + // Plaintext has no list items, so `1.` stays a line of its own there; + // the journal title is what this test is about. + for format in ["markdown", "org"] { + let out = pipe_stdin(input, &["--format", format]); + assert!( + out.status.success(), + "stderr={}", + String::from_utf8_lossy(&out.stderr) + ); + let result = String::from_utf8(out.stdout).unwrap(); + for line in [ + "1. R. Goswami, J. Chem. Phys. 150, 154105 (2019).\n", + "2. A. B. Author, SIAM J. Sci. Comput. 4, 5 (1983).\n", + "3. C. D. Author, ACM Trans. Math. Softw. 1, 2 (1975).\n", + ] { + assert!( + result.contains(line), + "{format}: journal title split, got:\n{result}" + ); + } + } +} + +#[test] +fn plaintext_journal_abbreviations_stay_in_the_sentence() { + let input = "Smith showed it in Front. Chem. 8, 589 (2020). \ + Jones followed in Modelling Simul. Mater. Sci. Eng. 12, 3 (2004). Done.\n"; + let out = pipe_stdin(input, &["--format", "plaintext"]); + assert!( + out.status.success(), + "stderr={}", + String::from_utf8_lossy(&out.stderr) + ); + let result = String::from_utf8(out.stdout).unwrap(); + assert_eq!( + result, + "Smith showed it in Front. Chem. 8, 589 (2020).\n\ + Jones followed in Modelling Simul. Mater. Sci. Eng. 12, 3 (2004).\n\ + Done.\n" + ); +}