@ampless/plugin-reading-time 0.1.0-alpha.1

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 ampless contributors
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.ja.md ADDED
@@ -0,0 +1,94 @@
1
+ > English: [README.md](./README.md)
2
+ >
3
+
4
+ # @ampless/plugin-reading-time
5
+
6
+ [ampless](https://github.com/heavymoons/ampless) 向け読了時間バッジプラグイン。投稿本文のテキストから読了時間を推定し、本文の前後に設定可能なラベルを挿入します。
7
+
8
+ > **プレリリース / アルファ版。** v1.0 まではマイナーバージョンでも破壊的変更が入る可能性があります。
9
+
10
+ バッジは `publicHtmlForPost` capability(Phase 6d)経由で出力されます。`ampless.publicHtmlForPost(post)` を呼ぶテーマなら自動的に描画されます。HTML はランタイムが `sanitize-html` で strict allowlist に沿ってサニタイズするため、テーマ側で `dangerouslySetInnerHTML` を使う必要はありません。
11
+
12
+ AWS のデータ権限は不要です。すべて公開 Next.js プロセスのリクエスト時に動作します。`trust_level` は `untrusted`。
13
+
14
+ ## インストール
15
+
16
+ ```bash
17
+ npm install @ampless/plugin-reading-time@alpha
18
+ ```
19
+
20
+ ## 設定
21
+
22
+ `cms.config.ts` に記述します:
23
+
24
+ ```ts
25
+ import { defineConfig } from 'ampless'
26
+ import readingTimePlugin from '@ampless/plugin-reading-time'
27
+
28
+ export default defineConfig({
29
+ // ...
30
+ plugins: [
31
+ readingTimePlugin(),
32
+ ],
33
+ })
34
+ ```
35
+
36
+ | オプション | デフォルト | 備考 |
37
+ |---|---|---|
38
+ | `wordsPerMinute` | `200` | 想定読書速度。英語の成人平均は 200〜250 WPM。 |
39
+ | `labelTemplate` | `'{minutes} min read'` | ラベルのテンプレート。`{minutes}` と `{words}` プレースホルダーを使用可能。 |
40
+ | `position` | `'beforeContent'` | `'beforeContent'` または `'afterContent'`。 |
41
+ | `instanceId` | `'reading-time'` | ランタイムのキー解決に使う namespace。同じサイトで 2 回登録する場合のみ変更する。 |
42
+
43
+ すべてのオプションはデプロイなしで `/admin/plugins → 読了時間` から編集できます。コンストラクタの値は初期デフォルトに過ぎません。
44
+
45
+ ## 設定項目(管理画面)
46
+
47
+ `/admin/plugins → 読了時間` から設定できます:
48
+
49
+ | キー | 型 | デフォルト | 備考 |
50
+ |---|---|---|---|
51
+ | `wordsPerMinute` | number | `200` | 最小 50、最大 1000。読書速度の仮定値。 |
52
+ | `labelTemplate` | text | `'{minutes} min read'` | `{minutes}` と `{words}` をプレースホルダーとして使用可能。最大 200 文字。 |
53
+ | `position` | select | `'beforeContent'` | `'beforeContent'` / `'afterContent'`。 |
54
+
55
+ ## 出力 HTML
56
+
57
+ プラグインは単一の `<p>` 要素を出力します:
58
+
59
+ ```html
60
+ <p class="ampless-reading-time" data-words="480" data-minutes="3">3 min read</p>
61
+ ```
62
+
63
+ - `data-words` — 生の語数(CJK 正規化後)。
64
+ - `data-minutes` — 推定読了時間(分)。常に 1 以上。
65
+ - クラス名 `ampless-reading-time` は安定しており、CSS によるスタイリングに利用できます。
66
+
67
+ ## 語数カウント
68
+
69
+ - **英語:** 空白区切りのトークン数。
70
+ - **CJK 文字**(漢字・ひらがな・カタカナ): 独立してカウントし、2 で除算して英語 WPM に合わせた換算値を算出。具体的には `CJK 文字数 / 2` の reading units を英語語数に加算します。
71
+ - **複合言語の投稿**: 両方のカウントを合算します。
72
+
73
+ フォーマット別テキスト抽出:
74
+
75
+ | `post.format` | テキスト抽出方法 |
76
+ |---|---|
77
+ | `tiptap` | JSON ツリーを再帰的に走査し、`text` ノードの値を結合。 |
78
+ | `markdown` | fenced code、インラインコード、画像・リンク構文、太字・斜体マーカー、HTML タグを除去。 |
79
+ | `html` | HTML タグを除去。 |
80
+ | `static` | 空文字を返す — バッジは出力されない。 |
81
+
82
+ ## ラベルの escape
83
+
84
+ ラベル文字列はプレースホルダー置換後に HTML escape されます。`< > & " '` は `&lt; &gt; &amp; &quot; &#39;` に変換されるため、管理画面の `labelTemplate` 設定に不等号が含まれていても XSS は発生しません。
85
+
86
+ ## トラストレベル
87
+
88
+ `untrusted`。`@ampless/runtime` が検証・サニタイズする HTML descriptor を返すだけです。DynamoDB、S3、Lambda プロセッサーには一切触れません。
89
+
90
+ ## v1 では対応しないこと
91
+
92
+ - **テーマ CSS** — `<p>` 要素には安定したクラス名(`ampless-reading-time`)が付与されるのでテーマ側でスタイリングできます。デフォルト CSS は注入しません。
93
+ - **ロケール別ラベル** — `labelTemplate` は全ロケール共通の単一文字列です。多言語サイトでは `instanceId` を別にしてプラグインを 2 回登録し、テーマ側でスロットを条件分岐させることで対応できます。
94
+ - **文字種別 WPM のカスタマイズ** — `wordsPerMinute` 設定は一律適用されます。アラビア語とラテン語で別レートを設定するなど、文字種別の細かな調整は deferred です。
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ > 日本語版: [README.ja.md](./README.ja.md)
2
+ >
3
+
4
+ # @ampless/plugin-reading-time
5
+
6
+ Reading-time badge plugin for [ampless](https://github.com/heavymoons/ampless). Estimates the reading time of a post from its body text and injects a configurable label before or after the post content.
7
+
8
+ > **Pre-release / alpha.** Breaking changes possible in any minor version until v1.0.
9
+
10
+ The badge is emitted via the `publicHtmlForPost` capability (Phase 6d). Themes that call `ampless.publicHtmlForPost(post)` automatically render it. The runtime sanitizes the HTML with `sanitize-html` under a strict allowlist before it reaches the page — no `dangerouslySetInnerHTML` needed in the theme.
11
+
12
+ No AWS data permissions are required — everything runs at request time inside the public Next.js process. The plugin's `trust_level` is `untrusted`.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @ampless/plugin-reading-time@alpha
18
+ ```
19
+
20
+ ## Configure
21
+
22
+ In `cms.config.ts`:
23
+
24
+ ```ts
25
+ import { defineConfig } from 'ampless'
26
+ import readingTimePlugin from '@ampless/plugin-reading-time'
27
+
28
+ export default defineConfig({
29
+ // ...
30
+ plugins: [
31
+ readingTimePlugin(),
32
+ ],
33
+ })
34
+ ```
35
+
36
+ | Option | Default | Notes |
37
+ |---|---|---|
38
+ | `wordsPerMinute` | `200` | Assumed reading speed. Typical adult reading speed is 200–250 WPM for English. |
39
+ | `labelTemplate` | `'{minutes} min read'` | Template for the label. Supports `{minutes}` and `{words}` placeholders. |
40
+ | `position` | `'beforeContent'` | `'beforeContent'` or `'afterContent'`. |
41
+ | `instanceId` | `'reading-time'` | Namespace used for runtime key resolution. Change only if registering the plugin twice. |
42
+
43
+ All options are also editable from `/admin/plugins → Reading time` without a redeploy — the constructor values above are just the initial defaults.
44
+
45
+ ## Settings (admin UI)
46
+
47
+ Configure from `/admin/plugins → Reading time`:
48
+
49
+ | Key | Type | Default | Notes |
50
+ |---|---|---|---|
51
+ | `wordsPerMinute` | number | `200` | Min 50, max 1000. Reading speed assumption. |
52
+ | `labelTemplate` | text | `'{minutes} min read'` | Use `{minutes}` and `{words}` as placeholders. Max 200 chars. |
53
+ | `position` | select | `'beforeContent'` | `'beforeContent'` / `'afterContent'`. |
54
+
55
+ ## Output HTML
56
+
57
+ The plugin emits a single `<p>` element:
58
+
59
+ ```html
60
+ <p class="ampless-reading-time" data-words="480" data-minutes="3">3 min read</p>
61
+ ```
62
+
63
+ - `data-words` — raw word count (after CJK normalization).
64
+ - `data-minutes` — computed reading time in minutes (always ≥ 1).
65
+ - The class `ampless-reading-time` is stable and suitable for CSS targeting.
66
+
67
+ ## Word counting
68
+
69
+ - **English:** whitespace-separated tokens.
70
+ - **CJK characters** (Han, Hiragana, Katakana): counted separately and divided by 2 to roughly match per-minute reading rates. Specifically: `CJK_chars / 2` reading units are added to the English word count.
71
+ - **Mixed language posts** combine both counts.
72
+
73
+ Format handling:
74
+
75
+ | `post.format` | How text is extracted |
76
+ |---|---|
77
+ | `tiptap` | Recursively walks the JSON tree, collecting `text` node values. |
78
+ | `markdown` | Strips fenced code, inline code, images, links syntax, bold/italic markers, and HTML tags. |
79
+ | `html` | Strips HTML tags. |
80
+ | `static` | Returns empty — no badge rendered. |
81
+
82
+ ## Label escaping
83
+
84
+ The label string is HTML-escaped after placeholder substitution. Characters `< > & " '` become `&lt; &gt; &amp; &quot; &#39;` respectively, preventing XSS even when a site operator stores angle brackets in the `labelTemplate` admin setting.
85
+
86
+ ## Trust level
87
+
88
+ `untrusted`. The plugin only emits an HTML descriptor validated and sanitized by `@ampless/runtime`. It does not access DynamoDB, S3, or any Lambda processor.
89
+
90
+ ## What it does not do (v1)
91
+
92
+ - **Theme CSS** — The `<p>` element carries a stable class name (`ampless-reading-time`) for theme authors to style. No default CSS is injected.
93
+ - **Locale-aware labels** — The `labelTemplate` is a single string shared across all locales. Multi-locale setups can register the plugin twice with distinct `instanceId` values and theme-side conditionally render the correct slot.
94
+ - **Custom WPM per script** — The `wordsPerMinute` setting applies uniformly. Fine-grained per-script tuning (e.g. different rates for Arabic vs. Latin) is deferred.
@@ -0,0 +1,85 @@
1
+ import { AmplessPlugin, Post } from 'ampless';
2
+ export { ContentFormat } from 'ampless';
3
+
4
+ interface ReadingTimeOptions {
5
+ /** Average words per minute. Default 200. */
6
+ wordsPerMinute?: number;
7
+ /**
8
+ * Template string for the label. Supports `{minutes}` and `{words}`
9
+ * placeholders. Default `'{minutes} min read'`.
10
+ */
11
+ labelTemplate?: string;
12
+ /**
13
+ * Where to inject the badge relative to the post body.
14
+ * Default `'beforeContent'`.
15
+ */
16
+ position?: 'beforeContent' | 'afterContent';
17
+ /**
18
+ * Optional namespace for this instance. Defaults to `'reading-time'`.
19
+ * Set a distinct value if registering the plugin more than once.
20
+ */
21
+ instanceId?: string;
22
+ }
23
+ /**
24
+ * Extract plain text from a post body. Returns an empty string for
25
+ * `format: 'static'` or unknown formats (safety — no badge for unreadable
26
+ * content).
27
+ */
28
+ declare function extractPlainText(post: Post): string;
29
+ /**
30
+ * Count the "reading units" in a plain-text string.
31
+ *
32
+ * - English words: whitespace-separated tokens (standard WPM baseline).
33
+ * - CJK characters (Han, Hiragana, Katakana): each 2 characters ≈ 1 reading
34
+ * unit (roughly half the per-minute rate vs. English), so we count them and
35
+ * divide by 2.
36
+ *
37
+ * For multilingual posts, both counts are summed to give a combined word
38
+ * estimate that feeds into the WPM calculation.
39
+ */
40
+ declare function countWords(text: string): number;
41
+ /**
42
+ * Coerce an unknown `wordsPerMinute` value (constructor option or stored
43
+ * setting) to a usable number inside the admin field's `[50, 1000]`
44
+ * range. Anything outside that — `0`, negative, `NaN`, `Infinity`, a
45
+ * non-number, or a string that can't parse — falls back to `fallback`.
46
+ *
47
+ * The admin UI's `min: 50, max: 1000` validators already gate
48
+ * hand-edited settings, but the constructor option is public API and
49
+ * a stored value can in theory be hand-edited in DynamoDB. Without
50
+ * this guard, `readingTimePlugin({ wordsPerMinute: 0 })` would emit
51
+ * `data-minutes="Infinity"` and `'Infinity min read'`.
52
+ */
53
+ declare function normalizeWpm(value: unknown, fallback: number): number;
54
+ /**
55
+ * Escape characters that have special meaning in HTML attribute values and
56
+ * text content. Applied to the rendered label string after placeholder
57
+ * substitution — prevents XSS when a `labelTemplate` or stored setting
58
+ * contains angle brackets or quotes.
59
+ *
60
+ * Only the five characters with HTML significance are replaced; no external
61
+ * library is needed for this narrow use case.
62
+ */
63
+ declare function escapeHtml(s: string): string;
64
+ /**
65
+ * Factory for the reading-time badge plugin. Returns a plugin that injects
66
+ * a `<p class="ampless-reading-time">` element before or after the post
67
+ * body via the `publicHtmlForPost` capability. The badge is omitted when the
68
+ * post has no readable content (e.g. `format: 'static'`, empty body).
69
+ *
70
+ * Usage in `cms.config.ts`:
71
+ *
72
+ * ```ts
73
+ * import readingTimePlugin from '@ampless/plugin-reading-time'
74
+ *
75
+ * export default defineConfig({
76
+ * plugins: [readingTimePlugin()],
77
+ * })
78
+ * ```
79
+ *
80
+ * All options are also editable from `/admin/plugins → Reading time` without
81
+ * a redeploy — the constructor values below are the initial defaults.
82
+ */
83
+ declare function readingTimePlugin(options?: ReadingTimeOptions): AmplessPlugin;
84
+
85
+ export { type ReadingTimeOptions, countWords, readingTimePlugin as default, escapeHtml, extractPlainText, normalizeWpm };
package/dist/index.js ADDED
@@ -0,0 +1,157 @@
1
+ // src/index.ts
2
+ import { definePlugin } from "ampless";
3
+ function extractTiptapText(node) {
4
+ if (!node || typeof node !== "object") return "";
5
+ const n = node;
6
+ if (n.type === "text" && typeof n.text === "string") {
7
+ return n.text;
8
+ }
9
+ if (Array.isArray(n.content)) {
10
+ return n.content.map((child) => extractTiptapText(child)).join(" ");
11
+ }
12
+ return "";
13
+ }
14
+ function extractPlainText(post) {
15
+ switch (post.format) {
16
+ case "tiptap": {
17
+ return extractTiptapText(post.body);
18
+ }
19
+ case "markdown": {
20
+ if (typeof post.body !== "string") return "";
21
+ return post.body.replace(/```[\s\S]*?```/g, " ").replace(/`[^`]*`/g, " ").replace(/^#{1,6}\s+/gm, "").replace(/!\[[^\]]*\]\([^)]*\)/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_~]{1,3}/g, "").replace(/<[^>]+>/g, " ");
22
+ }
23
+ case "html": {
24
+ if (typeof post.body !== "string") return "";
25
+ return post.body.replace(/<[^>]+>/g, " ");
26
+ }
27
+ case "static": {
28
+ return "";
29
+ }
30
+ default: {
31
+ return "";
32
+ }
33
+ }
34
+ }
35
+ function countWords(text) {
36
+ const trimmed = text.trim();
37
+ if (!trimmed) return 0;
38
+ const cjkMatches = trimmed.match(/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/gu);
39
+ const cjkChars = cjkMatches ? cjkMatches.length : 0;
40
+ const withoutCjk = trimmed.replace(
41
+ /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/gu,
42
+ " "
43
+ );
44
+ const englishMatches = withoutCjk.match(/[\p{L}\p{N}][\p{L}\p{N}'-]*/gu);
45
+ const englishWords = englishMatches ? englishMatches.length : 0;
46
+ return englishWords + Math.ceil(cjkChars / 2);
47
+ }
48
+ function normalizeWpm(value, fallback) {
49
+ const n = typeof value === "number" ? value : Number(value);
50
+ if (!Number.isFinite(n)) return fallback;
51
+ if (n < 50 || n > 1e3) return fallback;
52
+ return n;
53
+ }
54
+ function escapeHtml(s) {
55
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
56
+ }
57
+ function readingTimePlugin(options = {}) {
58
+ const {
59
+ wordsPerMinute: rawWpm = 200,
60
+ labelTemplate: defaultTemplate = "{minutes} min read",
61
+ position: defaultPosition = "beforeContent",
62
+ instanceId
63
+ } = options;
64
+ const defaultWpm = normalizeWpm(rawWpm, 200);
65
+ return definePlugin({
66
+ name: "reading-time",
67
+ packageName: "@ampless/plugin-reading-time",
68
+ instanceId,
69
+ displayName: { en: "Reading time", ja: "\u8AAD\u4E86\u6642\u9593" },
70
+ apiVersion: 1,
71
+ trust_level: "untrusted",
72
+ capabilities: ["publicHtmlForPost", "adminSettings"],
73
+ settings: {
74
+ public: [
75
+ {
76
+ type: "number",
77
+ key: "wordsPerMinute",
78
+ label: {
79
+ en: "Words per minute",
80
+ ja: "1 \u5206\u3042\u305F\u308A\u306E\u8A9E\u6570"
81
+ },
82
+ description: {
83
+ en: "Assumed reading speed. Typical adult reading speed is 200\u2013250 WPM for English, roughly 400\u2013500 characters per minute for Japanese.",
84
+ ja: "\u60F3\u5B9A\u8AAD\u66F8\u901F\u5EA6\u3002\u6210\u4EBA\u306E\u5E73\u5747\u306F\u82F1\u8A9E 200\u301C250 WPM\u3001\u65E5\u672C\u8A9E 400\u301C500 \u6587\u5B57/\u5206\u304C\u76EE\u5B89\u3002"
85
+ },
86
+ min: 50,
87
+ max: 1e3,
88
+ default: defaultWpm
89
+ },
90
+ {
91
+ type: "text",
92
+ key: "labelTemplate",
93
+ maxLength: 200,
94
+ label: {
95
+ en: "Label template",
96
+ ja: "\u30E9\u30D9\u30EB\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8"
97
+ },
98
+ description: {
99
+ en: "Template for the reading-time label. Use {minutes} for the minute count and {words} for the word count.",
100
+ ja: "\u30E9\u30D9\u30EB\u306E\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3002{minutes} \u3067\u5206\u6570\u3001{words} \u3067\u8A9E\u6570\u3092\u57CB\u3081\u8FBC\u3081\u307E\u3059\u3002"
101
+ },
102
+ default: defaultTemplate
103
+ },
104
+ {
105
+ type: "select",
106
+ key: "position",
107
+ label: {
108
+ en: "Position",
109
+ ja: "\u8868\u793A\u4F4D\u7F6E"
110
+ },
111
+ description: {
112
+ en: "Where to inject the reading-time badge relative to the post content.",
113
+ ja: "\u30D0\u30C3\u30B8\u3092\u672C\u6587\u306E\u524D\u5F8C\u3069\u3061\u3089\u306B\u633F\u5165\u3059\u308B\u304B\u3002"
114
+ },
115
+ options: [
116
+ {
117
+ value: "beforeContent",
118
+ label: { en: "Before content", ja: "\u672C\u6587\u306E\u524D" }
119
+ },
120
+ {
121
+ value: "afterContent",
122
+ label: { en: "After content", ja: "\u672C\u6587\u306E\u5F8C" }
123
+ }
124
+ ],
125
+ default: defaultPosition
126
+ }
127
+ ]
128
+ },
129
+ publicHtmlForPost(post, ctx) {
130
+ const wpm = normalizeWpm(ctx.setting("wordsPerMinute"), defaultWpm);
131
+ const template = (ctx.setting("labelTemplate") ?? "").trim() || defaultTemplate;
132
+ const position = ctx.setting("position") ?? defaultPosition;
133
+ const plainText = extractPlainText(post);
134
+ const words = countWords(plainText);
135
+ if (words === 0) return [];
136
+ const minutes = Math.max(1, Math.ceil(words / wpm));
137
+ const labelRaw = template.replace("{minutes}", String(minutes)).replace("{words}", String(words));
138
+ const escapedLabel = escapeHtml(labelRaw);
139
+ const body = `<p class="ampless-reading-time" data-words="${words}" data-minutes="${minutes}">${escapedLabel}</p>`;
140
+ return [
141
+ {
142
+ type: "html",
143
+ id: "display",
144
+ body,
145
+ position
146
+ }
147
+ ];
148
+ }
149
+ });
150
+ }
151
+ export {
152
+ countWords,
153
+ readingTimePlugin as default,
154
+ escapeHtml,
155
+ extractPlainText,
156
+ normalizeWpm
157
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@ampless/plugin-reading-time",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Reading-time badge plugin for ampless — estimates read time from post content and injects a configurable label before or after the post body",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/heavymoons/ampless.git",
24
+ "directory": "packages/plugin-reading-time"
25
+ },
26
+ "homepage": "https://github.com/heavymoons/ampless/tree/main/packages/plugin-reading-time#readme",
27
+ "bugs": "https://github.com/heavymoons/ampless/issues",
28
+ "dependencies": {
29
+ "ampless": "1.0.0-alpha.28"
30
+ },
31
+ "keywords": [
32
+ "ampless",
33
+ "plugin",
34
+ "ampless-plugin",
35
+ "reading-time",
36
+ "read-time",
37
+ "badge",
38
+ "cms"
39
+ ],
40
+ "amplessPlugin": {
41
+ "apiVersion": 1,
42
+ "name": "reading-time",
43
+ "trustLevel": "untrusted",
44
+ "capabilities": [
45
+ "publicHtmlForPost",
46
+ "adminSettings"
47
+ ],
48
+ "displayName": {
49
+ "en": "Reading time",
50
+ "ja": "読了時間"
51
+ }
52
+ },
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "dev": "tsup --watch",
56
+ "lint": "tsc --noEmit",
57
+ "test": "vitest run --passWithNoTests"
58
+ }
59
+ }