@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 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
@@ -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
+ }