@hanology/cham-browser 0.2.1 → 0.2.2
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/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# @hanology/cham-browser
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@hanology/cham-browser)
|
|
4
|
+
|
|
5
|
+
Site generator for [CHAM (Classical Han with Annotations Markup)](https://github.com/hanologyorg/cham-format) — generates a complete static website from CHAM content.
|
|
6
|
+
|
|
7
|
+
Includes parser, serializer, transformation pipeline, Vue 3 frontend template, and CLI.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @hanology/cham-browser
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## CLI
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Generate a static site from CHAM content
|
|
19
|
+
npx cham-browser --config config.yaml
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### config.yaml
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
# Branding
|
|
26
|
+
name: 漢流
|
|
27
|
+
nameEn: Hanology
|
|
28
|
+
subtitle: 古典詩文圖書館
|
|
29
|
+
|
|
30
|
+
# Content paths (relative to config.yaml)
|
|
31
|
+
libraryDir: library/content
|
|
32
|
+
authorsFile: library/data/authors.yaml
|
|
33
|
+
|
|
34
|
+
# Build options
|
|
35
|
+
outputDir: dist
|
|
36
|
+
pretty: true
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### Parser & Serializer (browser-compatible)
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { parse, serialize } from '@hanology/cham-browser'
|
|
45
|
+
|
|
46
|
+
const doc = parse(chamSource)
|
|
47
|
+
const output = serialize(doc)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Pipeline (pure functions, no Node.js fs)
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import {
|
|
54
|
+
buildPieceFromCham,
|
|
55
|
+
buildBookData,
|
|
56
|
+
buildLibraryIndex,
|
|
57
|
+
buildAuthorsJson,
|
|
58
|
+
buildDynastiesJson,
|
|
59
|
+
} from '@hanology/cham-browser'
|
|
60
|
+
|
|
61
|
+
const piece = buildPieceFromCham(
|
|
62
|
+
chamSource, bookConfig, authors, bookId,
|
|
63
|
+
proseFiles, layerFiles,
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Architecture
|
|
68
|
+
|
|
69
|
+
| Layer | Description |
|
|
70
|
+
|-------|-------------|
|
|
71
|
+
| `pipeline.ts` | Pure transformation functions (CHAM → JSON) |
|
|
72
|
+
| `cli.ts` | I/O adapter: reads files, calls pipeline, runs vite-ssg |
|
|
73
|
+
| `template/` | Vue 3 + vite-ssg frontend (components, views, styles) |
|
|
74
|
+
|
|
75
|
+
## Requirements
|
|
76
|
+
|
|
77
|
+
Node.js 20+
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
package/package.json
CHANGED
|
@@ -27,17 +27,22 @@ function layerLabel(ann: Annotation): string {
|
|
|
27
27
|
}
|
|
28
28
|
return ''
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
function onBackdropTouchMove() {
|
|
32
|
+
emit('close')
|
|
33
|
+
}
|
|
30
34
|
</script>
|
|
31
35
|
|
|
32
36
|
<template>
|
|
33
37
|
<Teleport to="body">
|
|
34
38
|
<Transition name="ann-fade">
|
|
35
|
-
<div v-if="visible && annotations.length" class="ann-backdrop" @click="emit('close')">
|
|
39
|
+
<div v-if="visible && annotations.length" class="ann-backdrop" @click="emit('close')" @touchmove="onBackdropTouchMove">
|
|
36
40
|
<div
|
|
37
41
|
class="ann-tooltip"
|
|
38
42
|
:class="{ 'ann-vertical': layout === 'vertical', 'ann-mobile-bottom': isMobile }"
|
|
39
43
|
:style="style"
|
|
40
44
|
@click.stop
|
|
45
|
+
@touchmove.stop
|
|
41
46
|
>
|
|
42
47
|
<button v-if="isMobile" class="ann-handle" @click="emit('close')">
|
|
43
48
|
<span class="ann-handle-bar" />
|
|
@@ -48,12 +53,10 @@ function layerLabel(ann: Annotation): string {
|
|
|
48
53
|
class="ann-entry"
|
|
49
54
|
:class="ann.kind"
|
|
50
55
|
>
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
<span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
|
|
54
|
-
</div>
|
|
56
|
+
<span class="ann-kind">{{ ann.kind === 'pronunciation' ? '音' : '義' }}</span>
|
|
57
|
+
<span v-if="layerLabel(ann)" class="ann-layer">{{ layerLabel(ann) }}</span>
|
|
55
58
|
<PronunciationGroup v-if="getSegment(ann)" :segment="getSegment(ann)!" />
|
|
56
|
-
<
|
|
59
|
+
<span v-else class="ann-body">{{ ann.text }}</span>
|
|
57
60
|
</div>
|
|
58
61
|
</div>
|
|
59
62
|
</div>
|
|
@@ -135,7 +138,6 @@ function layerLabel(ann: Annotation): string {
|
|
|
135
138
|
display: inline-flex;
|
|
136
139
|
align-items: center;
|
|
137
140
|
gap: 6px;
|
|
138
|
-
margin-bottom: 2px;
|
|
139
141
|
}
|
|
140
142
|
.ann-layer {
|
|
141
143
|
font-size: 11px;
|
|
@@ -153,7 +155,6 @@ function layerLabel(ann: Annotation): string {
|
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
.ann-body {
|
|
156
|
-
margin-top: 4px;
|
|
157
158
|
line-height: 1.8;
|
|
158
159
|
}
|
|
159
160
|
|
|
@@ -168,6 +169,7 @@ function layerLabel(ann: Annotation): string {
|
|
|
168
169
|
.ann-vertical .ann-entry {
|
|
169
170
|
margin-bottom: 0;
|
|
170
171
|
margin-left: 12px;
|
|
172
|
+
display: inline;
|
|
171
173
|
}
|
|
172
174
|
.ann-vertical .ann-kind {
|
|
173
175
|
margin-right: 0;
|
|
@@ -175,7 +177,6 @@ function layerLabel(ann: Annotation): string {
|
|
|
175
177
|
vertical-align: baseline;
|
|
176
178
|
}
|
|
177
179
|
.ann-vertical .ann-body {
|
|
178
|
-
margin-top: 0;
|
|
179
180
|
margin-left: 6px;
|
|
180
181
|
}
|
|
181
182
|
|
|
@@ -56,7 +56,8 @@ export function renderAnnotatedText(text: string, spans: AnnSpan[], useRuby = fa
|
|
|
56
56
|
const numText = toChineseNumber(annCounter)
|
|
57
57
|
const body = esc(text.slice(span.start, span.end))
|
|
58
58
|
if (useRuby) {
|
|
59
|
-
|
|
59
|
+
const rtCls = numText.length > 1 ? 'ann-num ann-num-long' : 'ann-num'
|
|
60
|
+
html += `<ruby class="ann-target ${kinds}" data-ann-ids="${ids}">${body}<rp></rp><rt class="${rtCls}">${numText}</rt><rp></rp></ruby>`
|
|
60
61
|
} else {
|
|
61
62
|
html += `<span class="ann-target ${kinds}" data-ann-ids="${ids}">${body}<sup class="ann-num">${numText}</sup></span>`
|
|
62
63
|
}
|
|
@@ -151,7 +152,18 @@ export function useAnnotationTooltip() {
|
|
|
151
152
|
|
|
152
153
|
function hide() { visible.value = false }
|
|
153
154
|
function toggle(event: MouseEvent, annotations: Annotation[]) {
|
|
154
|
-
if (visible.value) {
|
|
155
|
+
if (visible.value) {
|
|
156
|
+
const currentIds = items.value.map(a => a.id).sort().join(',')
|
|
157
|
+
const newIds = annotations.map(a => a.id).sort().join(',')
|
|
158
|
+
if (currentIds === newIds) {
|
|
159
|
+
// Same annotation: dismiss on mobile only (desktop uses hover to manage)
|
|
160
|
+
if (window.innerWidth < 768) hide()
|
|
161
|
+
} else {
|
|
162
|
+
show(event, annotations)
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
show(event, annotations)
|
|
166
|
+
}
|
|
155
167
|
}
|
|
156
168
|
|
|
157
169
|
return { visible, items, style, show, hide, toggle }
|
|
@@ -259,32 +259,33 @@ function tcy(n: number): string {
|
|
|
259
259
|
/>
|
|
260
260
|
</section>
|
|
261
261
|
|
|
262
|
-
<
|
|
263
|
-
num=""
|
|
264
|
-
label="注釋"
|
|
265
|
-
:special="false"
|
|
266
|
-
:text="piece.sections.annotations || ''"
|
|
267
|
-
:is-annotations="true"
|
|
268
|
-
:vertical="true"
|
|
269
|
-
class="v-section"
|
|
270
|
-
/>
|
|
271
|
-
|
|
272
|
-
<div v-if="hasLayers" class="v-layers-section">
|
|
273
|
-
<AnnotationLayerSelector
|
|
274
|
-
:layers="annotationLayers"
|
|
275
|
-
v-model:activeIds="activeLayerIds"
|
|
276
|
-
/>
|
|
262
|
+
<div class="v-section">
|
|
277
263
|
<SectionBlock
|
|
278
|
-
v-for="block in layerAnnotationBlocks"
|
|
279
|
-
:key="block.label"
|
|
280
264
|
num=""
|
|
281
|
-
|
|
265
|
+
label="注釋"
|
|
282
266
|
:special="false"
|
|
283
|
-
:text="
|
|
267
|
+
:text="piece.sections.annotations || ''"
|
|
284
268
|
:is-annotations="true"
|
|
285
269
|
:vertical="true"
|
|
286
|
-
class="v-section"
|
|
287
270
|
/>
|
|
271
|
+
<template v-if="hasLayers">
|
|
272
|
+
<div class="v-layers-inline">
|
|
273
|
+
<AnnotationLayerSelector
|
|
274
|
+
:layers="annotationLayers"
|
|
275
|
+
v-model:activeIds="activeLayerIds"
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
<SectionBlock
|
|
279
|
+
v-for="block in layerAnnotationBlocks"
|
|
280
|
+
:key="block.label"
|
|
281
|
+
num=""
|
|
282
|
+
:label="block.label"
|
|
283
|
+
:special="false"
|
|
284
|
+
:text="block.text"
|
|
285
|
+
:is-annotations="true"
|
|
286
|
+
:vertical="true"
|
|
287
|
+
/>
|
|
288
|
+
</template>
|
|
288
289
|
</div>
|
|
289
290
|
|
|
290
291
|
<SectionBlock
|
|
@@ -382,20 +383,32 @@ function tcy(n: number): string {
|
|
|
382
383
|
</div>
|
|
383
384
|
|
|
384
385
|
<div class="h-sections">
|
|
385
|
-
<div v-if="hasLayers" class="h-
|
|
386
|
-
<AnnotationLayerSelector
|
|
387
|
-
:layers="annotationLayers"
|
|
388
|
-
v-model:activeIds="activeLayerIds"
|
|
389
|
-
/>
|
|
386
|
+
<div v-if="piece.sections.annotations || hasLayers" class="h-ann-section">
|
|
390
387
|
<SectionBlock
|
|
391
|
-
v-
|
|
392
|
-
:key="block.label"
|
|
388
|
+
v-if="piece.sections.annotations"
|
|
393
389
|
num=""
|
|
394
|
-
|
|
390
|
+
label="注釋"
|
|
395
391
|
:special="false"
|
|
396
|
-
:text="
|
|
392
|
+
:text="piece.sections.annotations"
|
|
397
393
|
:is-annotations="true"
|
|
398
394
|
/>
|
|
395
|
+
<template v-if="hasLayers">
|
|
396
|
+
<div class="h-layers-inline">
|
|
397
|
+
<AnnotationLayerSelector
|
|
398
|
+
:layers="annotationLayers"
|
|
399
|
+
v-model:activeIds="activeLayerIds"
|
|
400
|
+
/>
|
|
401
|
+
</div>
|
|
402
|
+
<SectionBlock
|
|
403
|
+
v-for="block in layerAnnotationBlocks"
|
|
404
|
+
:key="block.label"
|
|
405
|
+
num=""
|
|
406
|
+
:label="block.label"
|
|
407
|
+
:special="false"
|
|
408
|
+
:text="block.text"
|
|
409
|
+
:is-annotations="true"
|
|
410
|
+
/>
|
|
411
|
+
</template>
|
|
399
412
|
</div>
|
|
400
413
|
|
|
401
414
|
<SectionBlock
|
|
@@ -532,15 +545,11 @@ function tcy(n: number): string {
|
|
|
532
545
|
flex-shrink: 0;
|
|
533
546
|
}
|
|
534
547
|
|
|
535
|
-
.v-layers-
|
|
536
|
-
flex-shrink: 0;
|
|
548
|
+
.v-layers-inline {
|
|
537
549
|
writing-mode: vertical-rl;
|
|
538
550
|
text-orientation: mixed;
|
|
539
|
-
padding: 0
|
|
540
|
-
|
|
541
|
-
flex-direction: column;
|
|
542
|
-
gap: 8px;
|
|
543
|
-
align-items: flex-start;
|
|
551
|
+
padding: 12px 0 4px;
|
|
552
|
+
border-top: 1px solid var(--border-light);
|
|
544
553
|
}
|
|
545
554
|
|
|
546
555
|
.v-source-link {
|
|
@@ -649,12 +658,13 @@ function tcy(n: number): string {
|
|
|
649
658
|
margin: 0 auto; padding-bottom: 80px;
|
|
650
659
|
}
|
|
651
660
|
|
|
652
|
-
.h-
|
|
661
|
+
.h-ann-section {
|
|
653
662
|
margin-bottom: 16px;
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.h-layers-inline {
|
|
666
|
+
padding: 12px 0;
|
|
667
|
+
margin-bottom: 8px;
|
|
658
668
|
}
|
|
659
669
|
|
|
660
670
|
.h-source-link {
|