@nkardaz/remark-typography 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/index.cjs +179 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +160 -0
- package/package.json +85 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yalla Nkardaz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# @nkardaz/remark-typography
|
|
2
|
+
|
|
3
|
+
A Remark plugin that automatically applies typography rules to MDX files.
|
|
4
|
+
Handles smart quotes, punctuation correction, non-breaking spaces, chemical
|
|
5
|
+
notation, ruby annotations, and more — driven by the
|
|
6
|
+
[@nkardaz/typography-rules](https://github.com/DemerNkardaz/typography-rules)
|
|
7
|
+
rule engine.
|
|
8
|
+
|
|
9
|
+
Built on [@nkardaz/typography-core](https://github.com/DemerNkardaz/typography-core).
|
|
10
|
+
Designed specifically for MDX. Correct behaviour with plain Markdown (`.md`)
|
|
11
|
+
files is not guaranteed.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm i -D @nkardaz/remark-typography
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> **Requires Node.js ≥ 24.0.0**
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
`vite.config.ts`
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import remarkTypography from '@nkardaz/remark-typography';
|
|
31
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
32
|
+
|
|
33
|
+
export default {
|
|
34
|
+
plugins: [
|
|
35
|
+
{
|
|
36
|
+
enforce: 'pre',
|
|
37
|
+
...mdx({
|
|
38
|
+
jsxImportSource: 'vue',
|
|
39
|
+
remarkPlugins: [
|
|
40
|
+
remarkFrontmatter,
|
|
41
|
+
[
|
|
42
|
+
remarkTypography,
|
|
43
|
+
{
|
|
44
|
+
locale: 'en',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
],
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Options
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
export type RemarkTypographyOptions = TypographyCoreOptions;
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
All options come from `@nkardaz/typography-core`. See its documentation for the full reference.
|
|
63
|
+
|
|
64
|
+
| Option | Type | Default | Description |
|
|
65
|
+
| --------------------- | ---------------------- | ------- | ---------------------------------------------------------------------------------------------- |
|
|
66
|
+
| `locale` | `string` | `'en'` | Default locale used for typography rules |
|
|
67
|
+
| `initTypographyRules` | `boolean` | `true` | Automatically registers all built-in rules from `@nkardaz/typography-rules` on plugin init |
|
|
68
|
+
| `initMarkupRules` | `boolean` | `false` | Automatically registers all built-in markup rules from `@nkardaz/typography-rules` on plugin init |
|
|
69
|
+
| `plugins` | `(() => () => void)[]` | `[]` | Custom rule plugins to register before processing. Each plugin is a factory returning a thunk |
|
|
70
|
+
| `logs` | `boolean` | `false` | Enables console warnings for missing locale rules and rule errors during processing |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Locale Resolution
|
|
75
|
+
|
|
76
|
+
The active locale is resolved in the following order of priority for each processed node:
|
|
77
|
+
|
|
78
|
+
1. **Per-node attribute** — `lang`, `language`, or `locale` on a JSX element
|
|
79
|
+
2. **Frontmatter key** — `locale`, `lang`, or `language` in the file's YAML frontmatter
|
|
80
|
+
3. **Plugin option** — `locale` passed to `remarkTypography()`
|
|
81
|
+
4. **Fallback** — `'en'`
|
|
82
|
+
|
|
83
|
+
### Per-file locale (frontmatter)
|
|
84
|
+
|
|
85
|
+
Supported frontmatter keys in order of priority: `locale` → `lang` → `language`.
|
|
86
|
+
|
|
87
|
+
```mdx
|
|
88
|
+
---
|
|
89
|
+
locale: ru
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
Текст на русском языке…
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> `remark-frontmatter` must be placed before `remarkTypography` in
|
|
96
|
+
> `remarkPlugins` for frontmatter locale detection to work.
|
|
97
|
+
|
|
98
|
+
### Per-node locale (JSX attribute)
|
|
99
|
+
|
|
100
|
+
Any JSX element can declare a locale via `lang`, `language`, or `locale`
|
|
101
|
+
attribute. Typography rules for that locale are applied only to text within
|
|
102
|
+
that element.
|
|
103
|
+
|
|
104
|
+
```mdx
|
|
105
|
+
<p lang="de">Ein schönes "Beispiel"</p>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Excluded Node Types
|
|
111
|
+
|
|
112
|
+
The following node types are never processed — their content is passed through unchanged:
|
|
113
|
+
|
|
114
|
+
| Node type | Reason |
|
|
115
|
+
| ------------ | ------------------------------------------- |
|
|
116
|
+
| `code` | Fenced code blocks |
|
|
117
|
+
| `inlineCode` | Inline code spans |
|
|
118
|
+
| `math` | Block math (remark-math) |
|
|
119
|
+
| `inlineMath` | Inline math (remark-math) |
|
|
120
|
+
| `html` | Raw HTML blocks |
|
|
121
|
+
| `yaml` | YAML frontmatter (consumed for locale only) |
|
|
122
|
+
| `toml` | TOML frontmatter |
|
|
123
|
+
|
|
124
|
+
Any node can also be excluded programmatically by setting `skipTypography: true`
|
|
125
|
+
on its `data` object:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// inside a remark plugin or unified transform
|
|
129
|
+
node.data = { ...node.data, skipTypography: true };
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Processing Pipeline
|
|
135
|
+
|
|
136
|
+
Each text node goes through two sequential phases:
|
|
137
|
+
|
|
138
|
+
**Phase 1 — String rules** (`replace`, `transform`, `function` rules returning `string`)
|
|
139
|
+
|
|
140
|
+
Text content is joined across sibling text nodes, wrapped with `protect()` to
|
|
141
|
+
shield URLs, emails, code spans, and other structured content from modification,
|
|
142
|
+
then all matching string rules are applied in weight order via `applyRules` from
|
|
143
|
+
`@nkardaz/typography-core`, and finally `unprotect()` restores the original protected spans.
|
|
144
|
+
|
|
145
|
+
**Phase 2 — Node rules** (`node` rules and `function` rules returning `Node[]`)
|
|
146
|
+
|
|
147
|
+
Text nodes that survived phase 1 are walked again. Rules that return a node
|
|
148
|
+
tree (e.g. `chemNotation`, `wrapWithTag`, `rubyText`) split text nodes into
|
|
149
|
+
mixed arrays of text and element nodes, which are spliced back into the parent's
|
|
150
|
+
children.
|
|
151
|
+
|
|
152
|
+
Both phases are applied per locale context. When a JSX element declares a `lang`, `language`, or `locale` attribute, a new locale context is pushed for that element's subtree before either phase runs — and popped on exit. Text inside `<p lang="ru">` goes through both phases with Russian rules, even if the surrounding file uses a different locale. See [Locale Resolution](#locale-resolution) for the full priority order.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Custom Plugins
|
|
157
|
+
|
|
158
|
+
Register additional rules using `@nkardaz/typography-rules` before passing the
|
|
159
|
+
plugin to `remarkTypography`.
|
|
160
|
+
|
|
161
|
+
`plugins/islenskaRules.ts`
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import { registerRule, newRule } from '@nkardaz/typography-rules';
|
|
165
|
+
import { smartQuotes } from '@nkardaz/typography-rules/functions';
|
|
166
|
+
import { PUNCTUATION } from '@nkardaz/typography-rules/glyphs';
|
|
167
|
+
|
|
168
|
+
export default function islenskaRules() {
|
|
169
|
+
return () => {
|
|
170
|
+
registerRule(
|
|
171
|
+
'is',
|
|
172
|
+
newRule('/islenska/typography/quotes', smartQuotes, [
|
|
173
|
+
{
|
|
174
|
+
outer: [
|
|
175
|
+
PUNCTUATION.is.leftSided.outerQuoteOpen,
|
|
176
|
+
PUNCTUATION.is.rightSided.outerQuoteClose,
|
|
177
|
+
],
|
|
178
|
+
inner: [
|
|
179
|
+
PUNCTUATION.is.leftSided.innerQuoteOpen,
|
|
180
|
+
PUNCTUATION.is.rightSided.innerQuoteClose,
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
], 100)
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
registerRule('is',
|
|
187
|
+
newRule('/islenska/numbers/1', /\b1\b/g, 'einn'),
|
|
188
|
+
newRule('/islenska/numbers/2', /\b2\b/g, 'tveir'),
|
|
189
|
+
newRule('/islenska/numbers/10', /\b10\b/g, 'tíu'),
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`vite.config.ts`
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import remarkTypography from '@nkardaz/remark-typography';
|
|
199
|
+
import islenskaRules from './plugins/islenskaRules';
|
|
200
|
+
|
|
201
|
+
export default {
|
|
202
|
+
plugins: [
|
|
203
|
+
{
|
|
204
|
+
enforce: 'pre',
|
|
205
|
+
...mdx({
|
|
206
|
+
remarkPlugins: [
|
|
207
|
+
[remarkTypography, { plugins: [islenskaRules], locale: 'is' }],
|
|
208
|
+
],
|
|
209
|
+
}),
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Building a Derived Plugin
|
|
218
|
+
|
|
219
|
+
`remarkTypography` is itself built with `createTypographyPlugin` from `@nkardaz/typography-core`.
|
|
220
|
+
If you need a plugin with different defaults or additional options, use the same factory directly
|
|
221
|
+
instead of wrapping `remarkTypography`:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import { createTypographyPlugin, type TypographyCoreOptions } from '@nkardaz/typography-core';
|
|
225
|
+
import { myRules } from './rules';
|
|
226
|
+
|
|
227
|
+
interface MyPluginOptions extends TypographyCoreOptions {
|
|
228
|
+
strictMode?: boolean;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export const myTypographyPlugin = createTypographyPlugin<MyPluginOptions, Root>({
|
|
232
|
+
defaultOptions: {
|
|
233
|
+
locale: 'de',
|
|
234
|
+
plugins: [myRules],
|
|
235
|
+
},
|
|
236
|
+
createHandler: (config) => (tree: Root) => {
|
|
237
|
+
// your MDX/remark AST traversal
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
See [@nkardaz/typography-core](https://github.com/DemerNkardaz/typography-core) for full factory documentation.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Plugin Order
|
|
247
|
+
|
|
248
|
+
### Place `remark-typography` AFTER these plugins
|
|
249
|
+
|
|
250
|
+
| Plugin | Reason |
|
|
251
|
+
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
252
|
+
| `remark-frontmatter` | Isolates YAML/TOML frontmatter and exposes it for locale detection |
|
|
253
|
+
| `remark-mdx-frontmatter` | Same — exports frontmatter as named exports |
|
|
254
|
+
| `remark-gfm` | Adds tables, strikethrough, task lists, autolinks — typography must see the final node structure |
|
|
255
|
+
| `remark-math` | Introduces `math` / `inlineMath` nodes — must exist before typography skips them |
|
|
256
|
+
| `remark-directive` | Adds container/leaf/inline directive nodes before typography processes remaining text |
|
|
257
|
+
| `remark-github` | Resolves mentions, issue refs, and commit links into nodes |
|
|
258
|
+
| `remark-footnotes` / `remark-gfm` (footnotes) | Footnote nodes must be created before text inside them is processed |
|
|
259
|
+
| `remark-extract-toc` / `remark-toc` | TOC is built from headings — headings must exist in the tree first |
|
|
260
|
+
| `remark-emoji` | Converts `:emoji:` shortcodes to Unicode — run before so output is not re-processed |
|
|
261
|
+
| `remark-breaks` | Converts soft breaks to `<br>` — structural change should precede text transformation |
|
|
262
|
+
| `remark-unwrap-images` | Moves image nodes up — structural, must precede text passes |
|
|
263
|
+
| `remark-mdx` | Parses MDX expressions and JSX nodes — their text content must be in the tree first |
|
|
264
|
+
|
|
265
|
+
### Place `remark-typography` BEFORE these plugins
|
|
266
|
+
|
|
267
|
+
| Plugin | Reason |
|
|
268
|
+
| --------------------------------------- | ------------------------------------------------------------------------------- |
|
|
269
|
+
| `remark-reading-time` | Counts words over text nodes — should see the final transformed text |
|
|
270
|
+
| `remark-reading-time-export` | Re-exports the reading time value — must come after the count is done |
|
|
271
|
+
| `remark-stringify` | Serializes the tree back to Markdown — must see final text |
|
|
272
|
+
| `remark-rehype` | Converts mdast → hast for HTML pipeline — carries final text values into rehype |
|
|
273
|
+
| `remark-mdx-export` / custom exporters | Export text content as JS variables — must reflect final typography |
|
|
274
|
+
|
|
275
|
+
### Order does not matter relative to these plugins
|
|
276
|
+
|
|
277
|
+
| Plugin | Reason |
|
|
278
|
+
| -------------------------------- | -------------------------------------------------------------------------------------- |
|
|
279
|
+
| `remark-slug` / `rehype-slug` | Operates on `id` generation from heading text — runs in rehype, separate pipeline |
|
|
280
|
+
| `remark-code-titles` | Parses code block meta strings, never touches text nodes |
|
|
281
|
+
| `remark-prism` / `remark-shiki` | Syntax highlighting — operates on code node values, which are excluded from typography |
|
|
282
|
+
| `remark-attr` | Parses inline attribute syntax `{.class}` — structural only, no text node mutation |
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## TypeScript
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import type { RemarkTypographyOptions } from '@nkardaz/remark-typography';
|
|
290
|
+
import type { TypographyCoreOptions, ResolvedCoreConfig } from '@nkardaz/typography-core';
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
| Type | Source | Description |
|
|
294
|
+
| ------------------------- | ------------------------ | ------------------------------------ |
|
|
295
|
+
| `RemarkTypographyOptions` | `@nkardaz/remark-typography` | Alias for `TypographyCoreOptions` |
|
|
296
|
+
| `TypographyCoreOptions` | `@nkardaz/typography-core` | Base options interface |
|
|
297
|
+
| `ResolvedCoreConfig` | `@nkardaz/typography-core` | Fully resolved config (all required) |
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
remarkTypography: () => remarkTypography
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
var import_unist_util_visit = require("unist-util-visit");
|
|
37
|
+
var import_js_yaml = __toESM(require("js-yaml"), 1);
|
|
38
|
+
var import_typography_rules = require("@nkardaz/typography-rules");
|
|
39
|
+
var import_helpers = require("@nkardaz/typography-rules/helpers");
|
|
40
|
+
var import_typography_core = require("@nkardaz/typography-core");
|
|
41
|
+
var EXCLUDED_TYPES = /* @__PURE__ */ new Set([
|
|
42
|
+
"code",
|
|
43
|
+
"inlineCode",
|
|
44
|
+
"math",
|
|
45
|
+
"inlineMath",
|
|
46
|
+
"html",
|
|
47
|
+
"yaml",
|
|
48
|
+
"toml"
|
|
49
|
+
]);
|
|
50
|
+
var JSX_TYPES = /* @__PURE__ */ new Set(["mdxJsxFlowElement", "mdxJsxTextElement"]);
|
|
51
|
+
function getJsxLang(node) {
|
|
52
|
+
for (const attr of node.attributes) {
|
|
53
|
+
if (attr.type === "mdxJsxAttribute" && (attr.name === "lang" || attr.name === "language" || attr.name === "locale") && typeof attr.value === "string") {
|
|
54
|
+
return attr.value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return void 0;
|
|
58
|
+
}
|
|
59
|
+
var remarkTypography = (0, import_typography_core.createTypographyPlugin)({
|
|
60
|
+
createHandler: (config) => (tree) => {
|
|
61
|
+
let frontmatterLocale;
|
|
62
|
+
(0, import_unist_util_visit.visit)(tree, "yaml", (node) => {
|
|
63
|
+
const data = import_js_yaml.default.load(node.value);
|
|
64
|
+
frontmatterLocale = (0, import_typography_core.getFrontmatterLocale)(data);
|
|
65
|
+
});
|
|
66
|
+
const fileLocale = frontmatterLocale ?? config.locale ?? "en";
|
|
67
|
+
function applyNodeRules(textNodes, parent, locale) {
|
|
68
|
+
const rules2 = (0, import_typography_rules.getWeightedRules)(locale).filter(
|
|
69
|
+
(r) => r.kind === "node" || r.kind === "function"
|
|
70
|
+
);
|
|
71
|
+
if (rules2.length === 0) return;
|
|
72
|
+
for (const textNode of textNodes) {
|
|
73
|
+
let current = [textNode];
|
|
74
|
+
for (const rule of rules2) {
|
|
75
|
+
if (rule.label && (0, import_typography_rules.isRuleDisabled)(rule.label)) continue;
|
|
76
|
+
const next = [];
|
|
77
|
+
for (const node of current) {
|
|
78
|
+
if (node.type !== "text") {
|
|
79
|
+
next.push(node);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
let nodeList;
|
|
83
|
+
if (rule.kind === "node") {
|
|
84
|
+
const nodeRule = rule;
|
|
85
|
+
nodeList = (0, import_typography_rules.htmlNode)(node.value, {
|
|
86
|
+
expression: nodeRule.rule,
|
|
87
|
+
nodes: nodeRule.nodes
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
const funcRule = rule;
|
|
91
|
+
const result = funcRule.rule(node.value, ...funcRule.args ?? []);
|
|
92
|
+
if (typeof result === "string" || !Array.isArray(result)) {
|
|
93
|
+
next.push(node);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
nodeList = result;
|
|
97
|
+
}
|
|
98
|
+
if (nodeList.length === 1 && nodeList[0].type === "text") {
|
|
99
|
+
next.push(node);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
for (const n of nodeList) {
|
|
103
|
+
next.push((0, import_typography_rules.nodeToMdast)(n));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
current = next;
|
|
107
|
+
}
|
|
108
|
+
if (current.length === 1 && current[0] === textNode) continue;
|
|
109
|
+
const index = parent.children.indexOf(textNode);
|
|
110
|
+
if (index !== -1) {
|
|
111
|
+
parent.children.splice(index, 1, ...current);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function processNode(node, localeStack) {
|
|
116
|
+
if (EXCLUDED_TYPES.has(node.type)) return;
|
|
117
|
+
if (node.data?.skipTypography) return;
|
|
118
|
+
const currentLocale = localeStack[localeStack.length - 1] ?? fileLocale;
|
|
119
|
+
if (JSX_TYPES.has(node.type)) {
|
|
120
|
+
const jsxNode = node;
|
|
121
|
+
const jsxLang = getJsxLang(jsxNode);
|
|
122
|
+
if (jsxLang) {
|
|
123
|
+
if (!(0, import_typography_rules.rulesHas)(fileLocale)) {
|
|
124
|
+
(0, import_typography_core.warning)(
|
|
125
|
+
!(0, import_typography_rules.rulesHas)("common") || (0, import_typography_rules.rulesCount)("common") === 0 ? `No rules registered for both of common and "${jsxLang}" locales on <${jsxNode.name ?? "unknown"}> node.` : `No rules registered for locale "${jsxLang}" on <${jsxNode.name ?? "unknown"}> node, only common rules will be applied.`,
|
|
126
|
+
config.logs
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
localeStack.push(jsxLang);
|
|
130
|
+
}
|
|
131
|
+
const jsxLocale = localeStack[localeStack.length - 1] ?? fileLocale;
|
|
132
|
+
if ("children" in jsxNode) {
|
|
133
|
+
const directTextNodes2 = jsxNode.children.filter(
|
|
134
|
+
(child) => child.type === "text"
|
|
135
|
+
);
|
|
136
|
+
if (directTextNodes2.length > 0) {
|
|
137
|
+
const combinedText = (0, import_helpers.joinNodes)(directTextNodes2);
|
|
138
|
+
const transformedText = (0, import_typography_core.applyRules)(combinedText, jsxLocale, { logs: config.logs });
|
|
139
|
+
(0, import_helpers.splitNodes)(transformedText, directTextNodes2);
|
|
140
|
+
applyNodeRules(directTextNodes2, jsxNode, jsxLocale);
|
|
141
|
+
}
|
|
142
|
+
for (const child of jsxNode.children) {
|
|
143
|
+
if (child.type !== "text") {
|
|
144
|
+
processNode(child, localeStack);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (jsxLang) localeStack.pop();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (!("children" in node)) return;
|
|
152
|
+
const parent = node;
|
|
153
|
+
const directTextNodes = parent.children.filter(
|
|
154
|
+
(child) => child.type === "text"
|
|
155
|
+
);
|
|
156
|
+
if (directTextNodes.length > 0) {
|
|
157
|
+
const combinedText = (0, import_helpers.joinNodes)(directTextNodes);
|
|
158
|
+
const transformedText = (0, import_typography_core.applyRules)(combinedText, currentLocale, { logs: config.logs });
|
|
159
|
+
(0, import_helpers.splitNodes)(transformedText, directTextNodes);
|
|
160
|
+
applyNodeRules(directTextNodes, parent, currentLocale);
|
|
161
|
+
}
|
|
162
|
+
for (const child of parent.children) {
|
|
163
|
+
if (child.type !== "text") {
|
|
164
|
+
processNode(child, localeStack);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!(0, import_typography_rules.rulesHas)(fileLocale)) {
|
|
169
|
+
(0, import_typography_core.warning)(
|
|
170
|
+
!(0, import_typography_rules.rulesHas)("common") || (0, import_typography_rules.rulesCount)("common") === 0 ? `No rules registered for both of common and "${fileLocale}" locales.` : `No rules registered for locale "${fileLocale}", only common rules will be applied.`,
|
|
171
|
+
config.logs
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const rules = (0, import_typography_rules.getWeightedRules)(fileLocale);
|
|
175
|
+
if (rules.length === 0) return;
|
|
176
|
+
processNode(tree, [fileLocale]);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Root } from 'mdast';
|
|
2
|
+
import { type TypographyCoreOptions } from '@nkardaz/typography-core';
|
|
3
|
+
export type RemarkTypographyOptions = TypographyCoreOptions;
|
|
4
|
+
export declare const remarkTypography: (options?: Partial<TypographyCoreOptions> | undefined) => (tree: Root) => void;
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { visit } from "unist-util-visit";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import {
|
|
5
|
+
getWeightedRules,
|
|
6
|
+
rulesCount,
|
|
7
|
+
rulesHas,
|
|
8
|
+
isRuleDisabled,
|
|
9
|
+
htmlNode,
|
|
10
|
+
nodeToMdast
|
|
11
|
+
} from "@nkardaz/typography-rules";
|
|
12
|
+
import { joinNodes, splitNodes } from "@nkardaz/typography-rules/helpers";
|
|
13
|
+
import {
|
|
14
|
+
createTypographyPlugin,
|
|
15
|
+
getFrontmatterLocale,
|
|
16
|
+
applyRules,
|
|
17
|
+
warning
|
|
18
|
+
} from "@nkardaz/typography-core";
|
|
19
|
+
var EXCLUDED_TYPES = /* @__PURE__ */ new Set([
|
|
20
|
+
"code",
|
|
21
|
+
"inlineCode",
|
|
22
|
+
"math",
|
|
23
|
+
"inlineMath",
|
|
24
|
+
"html",
|
|
25
|
+
"yaml",
|
|
26
|
+
"toml"
|
|
27
|
+
]);
|
|
28
|
+
var JSX_TYPES = /* @__PURE__ */ new Set(["mdxJsxFlowElement", "mdxJsxTextElement"]);
|
|
29
|
+
function getJsxLang(node) {
|
|
30
|
+
for (const attr of node.attributes) {
|
|
31
|
+
if (attr.type === "mdxJsxAttribute" && (attr.name === "lang" || attr.name === "language" || attr.name === "locale") && typeof attr.value === "string") {
|
|
32
|
+
return attr.value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
var remarkTypography = createTypographyPlugin({
|
|
38
|
+
createHandler: (config) => (tree) => {
|
|
39
|
+
let frontmatterLocale;
|
|
40
|
+
visit(tree, "yaml", (node) => {
|
|
41
|
+
const data = yaml.load(node.value);
|
|
42
|
+
frontmatterLocale = getFrontmatterLocale(data);
|
|
43
|
+
});
|
|
44
|
+
const fileLocale = frontmatterLocale ?? config.locale ?? "en";
|
|
45
|
+
function applyNodeRules(textNodes, parent, locale) {
|
|
46
|
+
const rules2 = getWeightedRules(locale).filter(
|
|
47
|
+
(r) => r.kind === "node" || r.kind === "function"
|
|
48
|
+
);
|
|
49
|
+
if (rules2.length === 0) return;
|
|
50
|
+
for (const textNode of textNodes) {
|
|
51
|
+
let current = [textNode];
|
|
52
|
+
for (const rule of rules2) {
|
|
53
|
+
if (rule.label && isRuleDisabled(rule.label)) continue;
|
|
54
|
+
const next = [];
|
|
55
|
+
for (const node of current) {
|
|
56
|
+
if (node.type !== "text") {
|
|
57
|
+
next.push(node);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
let nodeList;
|
|
61
|
+
if (rule.kind === "node") {
|
|
62
|
+
const nodeRule = rule;
|
|
63
|
+
nodeList = htmlNode(node.value, {
|
|
64
|
+
expression: nodeRule.rule,
|
|
65
|
+
nodes: nodeRule.nodes
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
const funcRule = rule;
|
|
69
|
+
const result = funcRule.rule(node.value, ...funcRule.args ?? []);
|
|
70
|
+
if (typeof result === "string" || !Array.isArray(result)) {
|
|
71
|
+
next.push(node);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
nodeList = result;
|
|
75
|
+
}
|
|
76
|
+
if (nodeList.length === 1 && nodeList[0].type === "text") {
|
|
77
|
+
next.push(node);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
for (const n of nodeList) {
|
|
81
|
+
next.push(nodeToMdast(n));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
current = next;
|
|
85
|
+
}
|
|
86
|
+
if (current.length === 1 && current[0] === textNode) continue;
|
|
87
|
+
const index = parent.children.indexOf(textNode);
|
|
88
|
+
if (index !== -1) {
|
|
89
|
+
parent.children.splice(index, 1, ...current);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function processNode(node, localeStack) {
|
|
94
|
+
if (EXCLUDED_TYPES.has(node.type)) return;
|
|
95
|
+
if (node.data?.skipTypography) return;
|
|
96
|
+
const currentLocale = localeStack[localeStack.length - 1] ?? fileLocale;
|
|
97
|
+
if (JSX_TYPES.has(node.type)) {
|
|
98
|
+
const jsxNode = node;
|
|
99
|
+
const jsxLang = getJsxLang(jsxNode);
|
|
100
|
+
if (jsxLang) {
|
|
101
|
+
if (!rulesHas(fileLocale)) {
|
|
102
|
+
warning(
|
|
103
|
+
!rulesHas("common") || rulesCount("common") === 0 ? `No rules registered for both of common and "${jsxLang}" locales on <${jsxNode.name ?? "unknown"}> node.` : `No rules registered for locale "${jsxLang}" on <${jsxNode.name ?? "unknown"}> node, only common rules will be applied.`,
|
|
104
|
+
config.logs
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
localeStack.push(jsxLang);
|
|
108
|
+
}
|
|
109
|
+
const jsxLocale = localeStack[localeStack.length - 1] ?? fileLocale;
|
|
110
|
+
if ("children" in jsxNode) {
|
|
111
|
+
const directTextNodes2 = jsxNode.children.filter(
|
|
112
|
+
(child) => child.type === "text"
|
|
113
|
+
);
|
|
114
|
+
if (directTextNodes2.length > 0) {
|
|
115
|
+
const combinedText = joinNodes(directTextNodes2);
|
|
116
|
+
const transformedText = applyRules(combinedText, jsxLocale, { logs: config.logs });
|
|
117
|
+
splitNodes(transformedText, directTextNodes2);
|
|
118
|
+
applyNodeRules(directTextNodes2, jsxNode, jsxLocale);
|
|
119
|
+
}
|
|
120
|
+
for (const child of jsxNode.children) {
|
|
121
|
+
if (child.type !== "text") {
|
|
122
|
+
processNode(child, localeStack);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (jsxLang) localeStack.pop();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!("children" in node)) return;
|
|
130
|
+
const parent = node;
|
|
131
|
+
const directTextNodes = parent.children.filter(
|
|
132
|
+
(child) => child.type === "text"
|
|
133
|
+
);
|
|
134
|
+
if (directTextNodes.length > 0) {
|
|
135
|
+
const combinedText = joinNodes(directTextNodes);
|
|
136
|
+
const transformedText = applyRules(combinedText, currentLocale, { logs: config.logs });
|
|
137
|
+
splitNodes(transformedText, directTextNodes);
|
|
138
|
+
applyNodeRules(directTextNodes, parent, currentLocale);
|
|
139
|
+
}
|
|
140
|
+
for (const child of parent.children) {
|
|
141
|
+
if (child.type !== "text") {
|
|
142
|
+
processNode(child, localeStack);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!rulesHas(fileLocale)) {
|
|
147
|
+
warning(
|
|
148
|
+
!rulesHas("common") || rulesCount("common") === 0 ? `No rules registered for both of common and "${fileLocale}" locales.` : `No rules registered for locale "${fileLocale}", only common rules will be applied.`,
|
|
149
|
+
config.logs
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const rules = getWeightedRules(fileLocale);
|
|
153
|
+
if (rules.length === 0) return;
|
|
154
|
+
processNode(tree, [fileLocale]);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
export {
|
|
158
|
+
remarkTypography
|
|
159
|
+
};
|
|
160
|
+
//# sourceMappingURL=index.mjs.map
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nkardaz/remark-typography",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Implements typography rules for MDX files.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Yalla Nkardaz",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"typography",
|
|
9
|
+
"typographic",
|
|
10
|
+
"text-formatting",
|
|
11
|
+
"typesetting",
|
|
12
|
+
"russian",
|
|
13
|
+
"english",
|
|
14
|
+
"text",
|
|
15
|
+
"mdast",
|
|
16
|
+
"markdown",
|
|
17
|
+
"unified",
|
|
18
|
+
"remark",
|
|
19
|
+
"remark-plugin"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/DemerNkardaz/remark-typography.git"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/DemerNkardaz/remark-typography#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/DemerNkardaz/remark-typography/issues"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "./dist/index.cjs",
|
|
31
|
+
"module": "./dist/index.mjs",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/index.mjs",
|
|
37
|
+
"require": "./dist/index.cjs"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"bundleSizeLimit": 20480,
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=24.0.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build:js": "node esbuild.config.mjs",
|
|
47
|
+
"build:types": "tsc --project tsconfig.build.json --emitDeclarationOnly",
|
|
48
|
+
"build": "npm run build:js && npm run build:types",
|
|
49
|
+
"dev": "esbuild --watch",
|
|
50
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
51
|
+
"lint:fix": "eslint src --ext .ts,.tsx --fix",
|
|
52
|
+
"format": "prettier --write \"src/**/*.{ts,tsx,json,md}\"",
|
|
53
|
+
"type-check": "tsc --noEmit",
|
|
54
|
+
"test": "vitest --run",
|
|
55
|
+
"test:coverage": "vitest --run --coverage",
|
|
56
|
+
"prepublishOnly": "npm run build && npm run lint && npm run type-check",
|
|
57
|
+
"knip": "knip"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@eslint/js": "^10.0.1",
|
|
61
|
+
"@types/js-yaml": "^4.0.9",
|
|
62
|
+
"@types/mdast": "^4.0.4",
|
|
63
|
+
"@types/mdx": "^2.0.14",
|
|
64
|
+
"@types/node": "^25.9.1",
|
|
65
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
66
|
+
"esbuild": "^0.28.0",
|
|
67
|
+
"eslint": "^10.4.1",
|
|
68
|
+
"eslint-config-prettier": "^10.1.8",
|
|
69
|
+
"knip": "^6.15.0",
|
|
70
|
+
"mdast-util-mdx-jsx": "^3.2.0",
|
|
71
|
+
"prettier": "^3.0.0",
|
|
72
|
+
"remark-parse": "^11.0.0",
|
|
73
|
+
"typescript": "^6.0.3",
|
|
74
|
+
"typescript-eslint": "^8.60.1",
|
|
75
|
+
"unified": "^11.0.5",
|
|
76
|
+
"vite-tsconfig-paths": "^6.1.1",
|
|
77
|
+
"vitest": "^4.1.8"
|
|
78
|
+
},
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"@nkardaz/typography-core": "1.0.1",
|
|
81
|
+
"@nkardaz/typography-rules": "1.0.0",
|
|
82
|
+
"js-yaml": "^4.2.0",
|
|
83
|
+
"unist-util-visit": "^5.1.0"
|
|
84
|
+
}
|
|
85
|
+
}
|