@hanology/cham-browser 0.1.0 → 0.2.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.
Files changed (43) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.js +191 -0
  3. package/dist/cli.js.map +1 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +2 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/pipeline.d.ts +14 -0
  8. package/dist/pipeline.js +377 -0
  9. package/dist/pipeline.js.map +1 -0
  10. package/package.json +22 -3
  11. package/template/index.html +29 -0
  12. package/template/src/App.vue +29 -0
  13. package/template/src/components/AnnotationLayerSelector.vue +66 -0
  14. package/template/src/components/AnnotationTooltip.vue +189 -0
  15. package/template/src/components/BookCard.vue +85 -0
  16. package/template/src/components/HorizontalDisplay.vue +100 -0
  17. package/template/src/components/PoemCard.vue +131 -0
  18. package/template/src/components/PronunciationGroup.vue +45 -0
  19. package/template/src/components/ReadingToolbar.vue +131 -0
  20. package/template/src/components/SectionBlock.vue +142 -0
  21. package/template/src/components/SideNav.vue +291 -0
  22. package/template/src/components/VerticalScroll.vue +120 -0
  23. package/template/src/composables/useAnnotationRenderer.ts +158 -0
  24. package/template/src/composables/useBook.ts +93 -0
  25. package/template/src/composables/useData.ts +41 -0
  26. package/template/src/composables/useHorizontalScroll.ts +60 -0
  27. package/template/src/composables/useLibrary.ts +40 -0
  28. package/template/src/composables/usePageLayout.ts +25 -0
  29. package/template/src/composables/useReadingMode.ts +70 -0
  30. package/template/src/composables/useTitle.ts +5 -0
  31. package/template/src/main.ts +22 -0
  32. package/template/src/router.ts +29 -0
  33. package/template/src/shims-vue.d.ts +7 -0
  34. package/template/src/styles/main.css +136 -0
  35. package/template/src/types.ts +164 -0
  36. package/template/src/utils/annotationParser.ts +58 -0
  37. package/template/src/utils/chineseNumber.ts +41 -0
  38. package/template/src/views/AuthorView.vue +338 -0
  39. package/template/src/views/BookHome.vue +375 -0
  40. package/template/src/views/LibraryHome.vue +419 -0
  41. package/template/src/views/PieceView.vue +793 -0
  42. package/src/index.ts +0 -20
  43. package/tsconfig.json +0 -16
@@ -0,0 +1,136 @@
1
+ /* ===== THEME DEFINITIONS ===== */
2
+ :root,
3
+ [data-theme="light"] {
4
+ --ink: #1a1a1a;
5
+ --ink-mid: #4a3f35;
6
+ --ink-light: #7a6e5f;
7
+ --ink-faint: #a89b8a;
8
+ --paper: #faf6ee;
9
+ --paper-warm: #f5f0e5;
10
+ --paper-deep: #ebe4d4;
11
+ --surface: #ffffff;
12
+ --surface-warm: #fffdf8;
13
+ --vermillion: #c23a2b;
14
+ --vermillion-light: #d4554a;
15
+ --gold: #9a7d3a;
16
+ --gold-light: #c9a84c;
17
+ --jade: #3a6b5e;
18
+ --border: #d8cdb8;
19
+ --border-light: #e8e0d0;
20
+ --shadow-rgb: 26,26,26;
21
+ --selection-text: #fff;
22
+ }
23
+
24
+ [data-theme="sepia"] {
25
+ --ink: #4a3f2e;
26
+ --ink-mid: #6b5d4a;
27
+ --ink-light: #8a7a66;
28
+ --ink-faint: #a8997f;
29
+ --paper: #f0e4c8;
30
+ --paper-warm: #e8d8b8;
31
+ --paper-deep: #dcc8a0;
32
+ --surface: #f8eed8;
33
+ --surface-warm: #f5ead0;
34
+ --vermillion: #a8321f;
35
+ --vermillion-light: #c44535;
36
+ --gold: #8a6d2a;
37
+ --gold-light: #b89540;
38
+ --jade: #2d5a4e;
39
+ --border: #c9b896;
40
+ --border-light: #d8cab0;
41
+ --shadow-rgb: 74,63,46;
42
+ --selection-text: #fff;
43
+ }
44
+
45
+ [data-theme="dark"] {
46
+ --ink: #e8e0d0;
47
+ --ink-mid: #c8bfae;
48
+ --ink-light: #9a9185;
49
+ --ink-faint: #6a6258;
50
+ --paper: #1c1c1e;
51
+ --paper-warm: #2a2a2c;
52
+ --paper-deep: #3a3a3c;
53
+ --surface: #2c2c2e;
54
+ --surface-warm: #323234;
55
+ --vermillion: #e05545;
56
+ --vermillion-light: #e87060;
57
+ --gold: #c9a84c;
58
+ --gold-light: #d8b860;
59
+ --jade: #5aaa98;
60
+ --border: #48484a;
61
+ --border-light: #555557;
62
+ --shadow-rgb: 0,0,0;
63
+ --selection-text: #fff;
64
+ }
65
+
66
+ [data-theme="oled"] {
67
+ --ink: #ffffff;
68
+ --ink-mid: #d0d0d0;
69
+ --ink-light: #909090;
70
+ --ink-faint: #606060;
71
+ --paper: #000000;
72
+ --paper-warm: #0a0a0a;
73
+ --paper-deep: #141414;
74
+ --surface: #0a0a0a;
75
+ --surface-warm: #101010;
76
+ --vermillion: #ff4444;
77
+ --vermillion-light: #ff6666;
78
+ --gold: #e8c840;
79
+ --gold-light: #f0d860;
80
+ --jade: #40c8a8;
81
+ --border: #333333;
82
+ --border-light: #444444;
83
+ --shadow-rgb: 0,0,0;
84
+ --selection-text: #000;
85
+ }
86
+
87
+ /* ===== RESET ===== */
88
+ :root {
89
+ --serif: 'Noto Serif TC', '宋體-繁', serif;
90
+ --sans: 'Noto Sans TC', 'PingFang TC', sans-serif;
91
+ --nav-width: 56px;
92
+ }
93
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
94
+ html { scroll-behavior: smooth; -webkit-font-smoothing: antialiased; }
95
+ body {
96
+ font-family: var(--serif);
97
+ background: var(--paper);
98
+ color: var(--ink);
99
+ line-height: 1.8;
100
+ min-height: 100vh;
101
+ }
102
+ ::selection { background: var(--vermillion); color: var(--selection-text); }
103
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
104
+ ::-webkit-scrollbar-track { background: transparent; }
105
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
106
+
107
+ a { color: inherit; text-decoration: none; }
108
+
109
+ /* ===== LOADING SCREEN ===== */
110
+ #app-loading {
111
+ position: fixed; inset: 0;
112
+ background: var(--paper);
113
+ display: flex; flex-direction: column;
114
+ align-items: center; justify-content: center;
115
+ z-index: 9999;
116
+ }
117
+ #app-loading .char {
118
+ font-family: var(--serif);
119
+ font-size: 64px; font-weight: 900;
120
+ color: var(--ink);
121
+ animation: pulse 1.2s ease-in-out infinite;
122
+ }
123
+ #app-loading .text {
124
+ font-family: var(--sans);
125
+ font-size: 13px; color: var(--ink-faint);
126
+ letter-spacing: 4px; margin-top: 16px;
127
+ }
128
+ @keyframes pulse {
129
+ 0%, 100% { opacity: 0.3; }
130
+ 50% { opacity: 1; }
131
+ }
132
+
133
+ /* ===== RESPONSIVE ===== */
134
+ @media (max-width: 768px) {
135
+ :root { --nav-width: 44px; }
136
+ }
@@ -0,0 +1,164 @@
1
+ // ─── Library ────────────────────────────────────────────────────
2
+
3
+ export type LibraryScale = 'single-piece' | 'single-book' | 'library'
4
+ export type BookGenre = 'poetry' | 'prose' | 'mixed' | 'drama'
5
+
6
+ export interface BookLayer {
7
+ id: string
8
+ label: string
9
+ shortLabel?: string
10
+ contributor: string
11
+ role?: string
12
+ nature?: string
13
+ displayOrder?: number
14
+ enabled?: boolean
15
+ }
16
+
17
+ export interface BookAnnotationDefaults {
18
+ defaultLabel?: string
19
+ defaultShortLabel?: string
20
+ }
21
+
22
+ export interface BookMeta {
23
+ id: string
24
+ title: string
25
+ subtitle?: string
26
+ titleEn?: string
27
+ publisher?: string
28
+ genre: BookGenre
29
+ count: number
30
+ hero?: string[]
31
+ layers?: BookLayer[]
32
+ annotation?: BookAnnotationDefaults
33
+ }
34
+
35
+ export interface CrossRef {
36
+ focusedBookId: string
37
+ focusedNum: number
38
+ fullBookId: string
39
+ fullNum?: number
40
+ relation: 'section' | 'excerpt'
41
+ }
42
+
43
+ export interface LibraryIndex {
44
+ scale: LibraryScale
45
+ books: BookMeta[]
46
+ crossRefs?: CrossRef[]
47
+ }
48
+
49
+ export interface BookData {
50
+ meta: BookMeta
51
+ pieces: Piece[]
52
+ }
53
+
54
+ // ─── Piece ──────────────────────────────────────────────────────
55
+
56
+ export interface TextRange {
57
+ type: 'point' | 'range' | 'full'
58
+ scope: 'verse' | 'title' | 'section' | 'full_text'
59
+ verseIndex?: number
60
+ sectionKey?: string
61
+ start?: number
62
+ end?: number
63
+ }
64
+
65
+ export interface Annotation {
66
+ id: string
67
+ range: TextRange
68
+ kind: 'pronunciation' | 'semantic' | 'etymology' | 'note' | 'definition' | 'commentary' | 'translation'
69
+ lang?: string
70
+ text: string
71
+ source: string
72
+ }
73
+
74
+ export interface PronSegment {
75
+ lang: 'yue' | 'cmn'
76
+ label: string
77
+ parts: string[]
78
+ }
79
+
80
+ export interface AnnotationEntry {
81
+ num: number
82
+ numDisplay: string
83
+ term: string
84
+ pronSegments: PronSegment[]
85
+ definition: string
86
+ }
87
+
88
+ export interface VerseLine {
89
+ text: string
90
+ }
91
+
92
+ export interface AnnotationLayer {
93
+ id: string
94
+ label: string
95
+ shortLabel: string
96
+ contributor: string
97
+ role: string
98
+ nature: string
99
+ displayOrder: number
100
+ enabled: boolean
101
+ annotations: Annotation[]
102
+ }
103
+
104
+ export interface PieceContributor {
105
+ id: string
106
+ name: string
107
+ role: string
108
+ title?: string
109
+ }
110
+
111
+ export interface PieceSource { text?: string
112
+ textRef?: string
113
+ pieceRef?: number
114
+ relation: 'section' | 'excerpt' | 'standalone'
115
+ range?: { start: string; end: string }
116
+ }
117
+
118
+ export interface ProseSection {
119
+ key: string
120
+ title: string
121
+ filename: string
122
+ body: string
123
+ order: number
124
+ }
125
+
126
+ export interface Piece {
127
+ bookId: string
128
+ num: number
129
+ title: string
130
+ author: string
131
+ authorId: string
132
+ dynasty: string
133
+ genre: BookGenre
134
+ verses: VerseLine[]
135
+ sections: Record<string, string>
136
+ structuredSections?: ProseSection[]
137
+ annotations: Annotation[]
138
+ layers?: Record<string, Annotation[]>
139
+ annotationLayers?: AnnotationLayer[]
140
+ source?: PieceSource
141
+ contributors?: PieceContributor[]
142
+ }
143
+
144
+ // Backward compatibility alias
145
+ export type Poem = Piece
146
+
147
+ // ─── Author & Dynasty ───────────────────────────────────────────
148
+
149
+ export interface Author {
150
+ '@id': string
151
+ '@type': string
152
+ name: string
153
+ dynasty: string
154
+ poemCount: number
155
+ bio?: string
156
+ }
157
+
158
+ export interface Dynasty {
159
+ '@id': string
160
+ '@type': string
161
+ name: string
162
+ authors: string[]
163
+ poemCount: number
164
+ }
@@ -0,0 +1,58 @@
1
+ import type { Annotation, AnnotationEntry, PronSegment } from '../types'
2
+ import { toChineseNumber } from './chineseNumber'
3
+
4
+ function esc(s: string): string {
5
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
6
+ }
7
+
8
+ export function parsePronSegments(def: string): { segments: PronSegment[], remaining: string } {
9
+ const parts = def.split('○').filter(s => s.trim())
10
+ if (!parts.length) return { segments: [], remaining: '' }
11
+
12
+ const segments: PronSegment[] = []
13
+ const remaining: string[] = []
14
+
15
+ for (const raw of parts) {
16
+ const s = raw.replace(/[;;]/g, ' ').trim()
17
+ if (s.startsWith('粵')) {
18
+ segments.push({ lang: 'yue', label: '粵', parts: s.slice(1).trim().split(/\s+/).filter(Boolean) })
19
+ } else if (s.startsWith('漢')) {
20
+ segments.push({ lang: 'cmn', label: '普', parts: s.slice(1).trim().split(/\s+/).filter(Boolean) })
21
+ } else {
22
+ remaining.push(s)
23
+ }
24
+ }
25
+
26
+ return { segments, remaining: remaining.join('\n').trim() }
27
+ }
28
+
29
+ export function parseAnnotationBlock(raw: string): AnnotationEntry[] {
30
+ const parts = raw.split(/(\d{1,2})\.\s*/)
31
+ const entries: AnnotationEntry[] = []
32
+
33
+ for (let i = 1; i < parts.length; i += 2) {
34
+ const num = parseInt(parts[i], 10)
35
+ const body = parts[i + 1]?.trim() || ''
36
+ const colonIdx = body.indexOf(':')
37
+ const term = colonIdx >= 0 ? body.slice(0, colonIdx) : ''
38
+ const def = colonIdx >= 0 ? body.slice(colonIdx + 1) : body
39
+ const { segments, remaining } = parsePronSegments(def)
40
+ entries.push({
41
+ num,
42
+ numDisplay: toChineseNumber(num),
43
+ term,
44
+ pronSegments: segments,
45
+ definition: esc(remaining),
46
+ })
47
+ }
48
+
49
+ return entries.length ? entries : []
50
+ }
51
+
52
+ export function annotationToPronSegment(ann: Annotation): PronSegment | null {
53
+ if (ann.kind !== 'pronunciation') return null
54
+ const parts = ann.text.replace(/[;;]/g, ' ').trim().split(/\s+/).filter(Boolean)
55
+ if (!parts.length) return null
56
+ const lang = ann.lang === 'yue' ? 'yue' : 'cmn'
57
+ return { lang, label: lang === 'yue' ? '粵' : '普', parts }
58
+ }
@@ -0,0 +1,41 @@
1
+ const DIGITS = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']
2
+
3
+ export function toChineseNumber(n: number): string {
4
+ if (n < 0) return '負' + toChineseNumber(-n)
5
+ if (n === 0) return '零'
6
+ if (n < 10) return DIGITS[n]
7
+
8
+ if (n >= 10000) {
9
+ const wan = Math.floor(n / 10000)
10
+ const rest = n % 10000
11
+ return (wan === 1 ? '' : toChineseNumber(wan)) + '萬' + (rest ? toChineseNumber(rest) : '')
12
+ }
13
+ if (n >= 1000) {
14
+ const qian = Math.floor(n / 1000)
15
+ const rest = n % 1000
16
+ return (qian === 1 ? '' : DIGITS[qian]) + '千' + (rest ? formatUnder1000(rest) : '')
17
+ }
18
+ if (n >= 100) {
19
+ const bai = Math.floor(n / 100)
20
+ const rest = n % 100
21
+ return (bai === 1 ? '' : DIGITS[bai]) + '百' + (rest ? formatTens(rest) : '')
22
+ }
23
+ return formatTens(n)
24
+ }
25
+
26
+ function formatTens(n: number): string {
27
+ if (n < 10) return DIGITS[n]
28
+ const tens = Math.floor(n / 10)
29
+ const ones = n % 10
30
+ const T: Record<number, string> = { 1: '十', 2: '廿', 3: '卅', 4: '卌' }
31
+ if (T[tens]) return T[tens] + (ones ? DIGITS[ones] : '')
32
+ return DIGITS[tens] + (ones ? DIGITS[ones] : '十')
33
+ }
34
+
35
+ function formatUnder1000(n: number): string {
36
+ if (n === 0) return ''
37
+ if (n < 100) return formatTens(n)
38
+ const bai = Math.floor(n / 100)
39
+ const rest = n % 100
40
+ return DIGITS[bai] + '百' + (rest ? formatTens(rest) : '')
41
+ }