@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 +21 -0
- package/README.ja.md +94 -0
- package/README.md +94 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +157 -0
- package/package.json +59 -0
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 されます。`< > & " '` は `< > & " '` に変換されるため、管理画面の `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 `< > & " '` 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.
|
package/dist/index.d.ts
ADDED
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
+
}
|