@vcgstudiosy-beep/dlof 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 +21 -0
- package/README.md +103 -0
- package/bin/dlof.js +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +237 -0
- package/dist/dlof/parseDlof.d.ts +6 -0
- package/dist/dlof/parseDlof.js +181 -0
- package/dist/dlof/serializeDlof.d.ts +3 -0
- package/dist/dlof/serializeDlof.js +176 -0
- package/dist/dlof/validate.d.ts +11 -0
- package/dist/dlof/validate.js +76 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +47 -0
- package/dist/loop/loopUtils.d.ts +16 -0
- package/dist/loop/loopUtils.js +66 -0
- package/dist/packages/dlofSeries.d.ts +9 -0
- package/dist/packages/dlofSeries.js +60 -0
- package/dist/packages/dlofpkg.d.ts +10 -0
- package/dist/packages/dlofpkg.js +72 -0
- package/dist/player/renderPlayerHtml.d.ts +13 -0
- package/dist/player/renderPlayerHtml.js +235 -0
- package/dist/types.d.ts +141 -0
- package/dist/types.js +6 -0
- package/dist/viewer/renderContent.d.ts +3 -0
- package/dist/viewer/renderContent.js +77 -0
- package/dist/viewer/renderViewerHtml.d.ts +11 -0
- package/dist/viewer/renderViewerHtml.js +46 -0
- package/dist/viewer/theme.d.ts +13 -0
- package/dist/viewer/theme.js +102 -0
- package/dist/xml/parser.d.ts +22 -0
- package/dist/xml/parser.js +199 -0
- package/dist/xml/serializer.d.ts +11 -0
- package/dist/xml/serializer.js +46 -0
- package/dist/zip/crc32.d.ts +2 -0
- package/dist/zip/crc32.js +22 -0
- package/dist/zip/zipReader.d.ts +14 -0
- package/dist/zip/zipReader.js +109 -0
- package/dist/zip/zipWriter.d.ts +9 -0
- package/dist/zip/zipWriter.js +106 -0
- package/package.json +42 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderPlayerHtml = renderPlayerHtml;
|
|
4
|
+
const theme_1 = require("../viewer/theme");
|
|
5
|
+
const renderContent_1 = require("../viewer/renderContent");
|
|
6
|
+
const loopUtils_1 = require("../loop/loopUtils");
|
|
7
|
+
const DEFAULT_MAX_INLINE = 20 * 1024 * 1024;
|
|
8
|
+
const MIME_BY_EXT = {
|
|
9
|
+
mp4: "video/mp4",
|
|
10
|
+
webm: "video/webm",
|
|
11
|
+
mov: "video/quicktime",
|
|
12
|
+
mkv: "video/x-matroska",
|
|
13
|
+
mp3: "audio/mpeg",
|
|
14
|
+
wav: "audio/wav",
|
|
15
|
+
ogg: "audio/ogg",
|
|
16
|
+
m4a: "audio/mp4",
|
|
17
|
+
aac: "audio/aac",
|
|
18
|
+
flac: "audio/flac",
|
|
19
|
+
};
|
|
20
|
+
function guessMime(path) {
|
|
21
|
+
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
22
|
+
return MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
23
|
+
}
|
|
24
|
+
function isVideo(mime) {
|
|
25
|
+
return mime.startsWith("video/");
|
|
26
|
+
}
|
|
27
|
+
function isAudio(mime) {
|
|
28
|
+
return mime.startsWith("audio/");
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* مشغّل dlof: يبني تطبيق HTML/JS تفاعلي واحد لسلسلة أو حلقة كاملة من ملفات .dlof.
|
|
32
|
+
* يوفر: قائمة تصفح جانبية، أزرار التالي/السابق (تتبع loopLinks)، تشغيل الوسائط
|
|
33
|
+
* المرجعية (mediaRef / mediaFolder) مع شريط تقدّم قابل للسحب، وحفظ آخر موضع
|
|
34
|
+
* تصفح في التخزين المحلي للمتصفح بين الجلسات.
|
|
35
|
+
*/
|
|
36
|
+
function renderPlayerHtml(series, options = {}) {
|
|
37
|
+
const chain = (0, loopUtils_1.resolveLoopChain)(series.documents, series.rootFileName);
|
|
38
|
+
if (!chain.length) {
|
|
39
|
+
throw new Error("لا يمكن بناء المشغّل: لم يُعثر على أي حلقة صالحة من المستندات");
|
|
40
|
+
}
|
|
41
|
+
const maxInline = options.maxInlineMediaBytes ?? DEFAULT_MAX_INLINE;
|
|
42
|
+
const firstDoc = chain[0].document;
|
|
43
|
+
const theme = (0, theme_1.resolveTheme)(firstDoc.template);
|
|
44
|
+
const title = options.title ?? firstDoc.metadata.title ?? series.name;
|
|
45
|
+
const items = chain.map(({ fileName, document }) => {
|
|
46
|
+
let mediaHtml = "";
|
|
47
|
+
const mediaRef = document.content.kind === "episodeItem" ? document.content.mediaRef : undefined;
|
|
48
|
+
if (mediaRef) {
|
|
49
|
+
const normalized = mediaRef.replace(/^\.?\//, "");
|
|
50
|
+
const asset = series.assets[normalized] ?? series.assets[normalized.split("/").pop() ?? ""];
|
|
51
|
+
const mime = guessMime(normalized);
|
|
52
|
+
if (asset && asset.length <= maxInline && (isVideo(mime) || isAudio(mime))) {
|
|
53
|
+
const dataUrl = `data:${mime};base64,${asset.toString("base64")}`;
|
|
54
|
+
mediaHtml = isVideo(mime)
|
|
55
|
+
? `<video class="player-media" data-role="media" src="${dataUrl}"></video>`
|
|
56
|
+
: `<audio class="player-media" data-role="media" src="${dataUrl}"></audio>`;
|
|
57
|
+
}
|
|
58
|
+
else if (asset) {
|
|
59
|
+
mediaHtml = `<p class="tag">⚠️ ملف الوسائط "${(0, theme_1.escapeHtml)(normalized)}" أكبر من الحد المسموح للتضمين المباشر في هذا المشغّل.</p>`;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
mediaHtml = `<p class="tag">📁 مرجع وسائط خارجي: ${(0, theme_1.escapeHtml)(normalized)}</p>`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
fileName,
|
|
67
|
+
title: document.metadata.title,
|
|
68
|
+
navLabel: document.content.kind === "episodeItem"
|
|
69
|
+
? document.content.episodeTitle
|
|
70
|
+
: document.metadata.title,
|
|
71
|
+
duration: document.content.kind === "episodeItem" ? document.content.duration ?? 0 : 0,
|
|
72
|
+
cardHtml: (0, renderContent_1.renderDocumentCard)(document, theme.layout),
|
|
73
|
+
mediaHtml,
|
|
74
|
+
hasMedia: !!mediaHtml && mediaHtml.includes('data-role="media"'),
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
const navListHtml = items
|
|
78
|
+
.map((it, idx) => `<li data-index="${idx}" class="nav-item${idx === 0 ? " active" : ""}">
|
|
79
|
+
<span class="nav-num">${idx + 1}</span>
|
|
80
|
+
<span class="nav-title">${(0, theme_1.escapeHtml)(it.navLabel || it.title || it.fileName)}</span>
|
|
81
|
+
</li>`)
|
|
82
|
+
.join("");
|
|
83
|
+
const panelsHtml = items
|
|
84
|
+
.map((it, idx) => `<section class="panel" data-index="${idx}" style="${idx === 0 ? "" : "display:none"}">
|
|
85
|
+
${it.mediaHtml}
|
|
86
|
+
${it.hasMedia
|
|
87
|
+
? `<div class="progress-wrap">
|
|
88
|
+
<input type="range" class="seek" min="0" max="1000" value="0" data-role="seek"/>
|
|
89
|
+
<div class="time-row"><span data-role="current">0:00</span><span data-role="duration">0:00</span></div>
|
|
90
|
+
</div>`
|
|
91
|
+
: ""}
|
|
92
|
+
${it.cardHtml}
|
|
93
|
+
</section>`)
|
|
94
|
+
.join("");
|
|
95
|
+
const playerCss = `
|
|
96
|
+
.layout { display: flex; gap: 24px; align-items: flex-start; }
|
|
97
|
+
.sidebar {
|
|
98
|
+
width: 220px; flex-shrink: 0; max-height: 80vh; overflow-y: auto;
|
|
99
|
+
border-inline-start: 1px solid rgba(0,0,0,.08); padding-inline-start: 12px;
|
|
100
|
+
}
|
|
101
|
+
.nav-item {
|
|
102
|
+
display: flex; align-items: center; gap: 8px; padding: 8px 6px;
|
|
103
|
+
border-radius: 10px; cursor: pointer; font-size: .9rem; list-style: none;
|
|
104
|
+
}
|
|
105
|
+
.nav-item:hover { background: rgba(0,0,0,.05); }
|
|
106
|
+
.nav-item.active { background: var(--primary); color: #fff; }
|
|
107
|
+
.nav-num {
|
|
108
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
109
|
+
width: 22px; height: 22px; border-radius: 50%; background: rgba(0,0,0,.08);
|
|
110
|
+
font-size: .75rem; flex-shrink: 0;
|
|
111
|
+
}
|
|
112
|
+
.nav-item.active .nav-num { background: rgba(255,255,255,.3); }
|
|
113
|
+
ul#navList { padding: 0; margin: 0; }
|
|
114
|
+
.main { flex: 1; min-width: 0; }
|
|
115
|
+
.player-media { width: 100%; border-radius: 14px; background: #000; margin-bottom: 4px; }
|
|
116
|
+
.progress-wrap { margin-bottom: 16px; }
|
|
117
|
+
.seek { width: 100%; accent-color: var(--primary); }
|
|
118
|
+
.time-row { display: flex; justify-content: space-between; font-size: .75rem; opacity: .7; }
|
|
119
|
+
@media (max-width: 720px) {
|
|
120
|
+
.layout { flex-direction: column; }
|
|
121
|
+
.sidebar { width: 100%; max-height: 220px; border-inline-start: none; border-bottom: 1px solid rgba(0,0,0,.08); padding-bottom: 8px; }
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
const script = `
|
|
125
|
+
const STATE_KEY = "dlof-player-progress::${(0, theme_1.escapeHtml)(series.name)}";
|
|
126
|
+
let current = 0;
|
|
127
|
+
const total = ${items.length};
|
|
128
|
+
const navItems = document.querySelectorAll(".nav-item");
|
|
129
|
+
const panels = document.querySelectorAll(".panel");
|
|
130
|
+
const prevBtn = document.getElementById("prevBtn");
|
|
131
|
+
const nextBtn = document.getElementById("nextBtn");
|
|
132
|
+
|
|
133
|
+
function fmt(t) {
|
|
134
|
+
if (!isFinite(t)) return "0:00";
|
|
135
|
+
const m = Math.floor(t / 60), s = Math.floor(t % 60);
|
|
136
|
+
return m + ":" + String(s).padStart(2, "0");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function setupMedia(panel) {
|
|
140
|
+
const media = panel.querySelector('[data-role="media"]');
|
|
141
|
+
const seek = panel.querySelector('[data-role="seek"]');
|
|
142
|
+
const curEl = panel.querySelector('[data-role="current"]');
|
|
143
|
+
const durEl = panel.querySelector('[data-role="duration"]');
|
|
144
|
+
if (!media || !seek) return;
|
|
145
|
+
let seeking = false;
|
|
146
|
+
media.addEventListener("loadedmetadata", () => { durEl.textContent = fmt(media.duration); });
|
|
147
|
+
media.addEventListener("timeupdate", () => {
|
|
148
|
+
if (!seeking) seek.value = String(Math.floor((media.currentTime / (media.duration || 1)) * 1000));
|
|
149
|
+
curEl.textContent = fmt(media.currentTime);
|
|
150
|
+
savePosition(media.currentTime);
|
|
151
|
+
});
|
|
152
|
+
seek.addEventListener("input", () => { seeking = true; });
|
|
153
|
+
seek.addEventListener("change", () => {
|
|
154
|
+
media.currentTime = (Number(seek.value) / 1000) * (media.duration || 0);
|
|
155
|
+
seeking = false;
|
|
156
|
+
});
|
|
157
|
+
media.addEventListener("ended", () => { if (current < total - 1) goTo(current + 1); });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function savePosition(t) {
|
|
161
|
+
try {
|
|
162
|
+
const data = JSON.parse(localStorage.getItem(STATE_KEY) || "{}");
|
|
163
|
+
data[current] = t;
|
|
164
|
+
data.lastIndex = current;
|
|
165
|
+
localStorage.setItem(STATE_KEY, JSON.stringify(data));
|
|
166
|
+
} catch (e) {}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function goTo(index) {
|
|
170
|
+
if (index < 0 || index >= total) return;
|
|
171
|
+
current = index;
|
|
172
|
+
navItems.forEach((el) => el.classList.toggle("active", Number(el.dataset.index) === index));
|
|
173
|
+
panels.forEach((el) => {
|
|
174
|
+
const match = Number(el.dataset.index) === index;
|
|
175
|
+
el.style.display = match ? "" : "none";
|
|
176
|
+
if (match) setupMedia(el);
|
|
177
|
+
});
|
|
178
|
+
prevBtn.disabled = index === 0;
|
|
179
|
+
nextBtn.disabled = index === total - 1;
|
|
180
|
+
try {
|
|
181
|
+
const data = JSON.parse(localStorage.getItem(STATE_KEY) || "{}");
|
|
182
|
+
data.lastIndex = index;
|
|
183
|
+
localStorage.setItem(STATE_KEY, JSON.stringify(data));
|
|
184
|
+
const media = panels[index].querySelector('[data-role="media"]');
|
|
185
|
+
if (media && data[index]) media.currentTime = data[index];
|
|
186
|
+
} catch (e) {}
|
|
187
|
+
panels[index].scrollIntoView({ behavior: "smooth", block: "start" });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
navItems.forEach((el) => el.addEventListener("click", () => goTo(Number(el.dataset.index))));
|
|
191
|
+
prevBtn.addEventListener("click", () => goTo(current - 1));
|
|
192
|
+
nextBtn.addEventListener("click", () => goTo(current + 1));
|
|
193
|
+
document.addEventListener("keydown", (e) => {
|
|
194
|
+
if (e.key === "ArrowRight") goTo(current - 1); // RTL: يمين = السابق
|
|
195
|
+
if (e.key === "ArrowLeft") goTo(current + 1); // RTL: يسار = التالي
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// استئناف آخر موضع تصفح محفوظ
|
|
199
|
+
try {
|
|
200
|
+
const data = JSON.parse(localStorage.getItem(STATE_KEY) || "{}");
|
|
201
|
+
if (typeof data.lastIndex === "number") goTo(data.lastIndex);
|
|
202
|
+
else setupMedia(panels[0]);
|
|
203
|
+
} catch (e) { setupMedia(panels[0]); }
|
|
204
|
+
prevBtn.disabled = current === 0;
|
|
205
|
+
nextBtn.disabled = current === total - 1;
|
|
206
|
+
`;
|
|
207
|
+
return `<!DOCTYPE html>
|
|
208
|
+
<html lang="${(0, theme_1.escapeHtml)(firstDoc.metadata.language ?? "ar")}" dir="rtl">
|
|
209
|
+
<head>
|
|
210
|
+
<meta charset="UTF-8"/>
|
|
211
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
212
|
+
<title>${(0, theme_1.escapeHtml)(title)} — مشغّل DLoF</title>
|
|
213
|
+
<style>${(0, theme_1.baseCss)(theme)}${playerCss}</style>
|
|
214
|
+
</head>
|
|
215
|
+
<body>
|
|
216
|
+
<div class="app">
|
|
217
|
+
<div class="badge">DLoF Player</div>
|
|
218
|
+
<h1 style="margin-top:6px">${(0, theme_1.escapeHtml)(title)}</h1>
|
|
219
|
+
<div class="layout">
|
|
220
|
+
<aside class="sidebar">
|
|
221
|
+
<ul id="navList">${navListHtml}</ul>
|
|
222
|
+
</aside>
|
|
223
|
+
<main class="main">
|
|
224
|
+
${panelsHtml}
|
|
225
|
+
<div class="footer-nav">
|
|
226
|
+
<button id="prevBtn" class="nav-btn">◀ السابق</button>
|
|
227
|
+
<button id="nextBtn" class="nav-btn secondary">التالي ▶</button>
|
|
228
|
+
</div>
|
|
229
|
+
</main>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<script>${script}</script>
|
|
233
|
+
</body>
|
|
234
|
+
</html>`;
|
|
235
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* الأنواع الأساسية لصيغة DLoF (Document Loop Format)
|
|
3
|
+
* مطابقة لـ docs/SPECIFICATION.md و spec/schema/dlof.xsd
|
|
4
|
+
*/
|
|
5
|
+
export type Domain = "education" | "book" | "infoApp" | "infoLoop" | "series" | "custom";
|
|
6
|
+
export interface Metadata {
|
|
7
|
+
title: string;
|
|
8
|
+
domain: Domain | string;
|
|
9
|
+
author?: string;
|
|
10
|
+
createdAt?: string;
|
|
11
|
+
updatedAt?: string;
|
|
12
|
+
language?: string;
|
|
13
|
+
tags?: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface LoopLink {
|
|
16
|
+
ref: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface LoopLinks {
|
|
20
|
+
previous?: LoopLink;
|
|
21
|
+
next?: LoopLink;
|
|
22
|
+
loopRoot: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface GenericItem {
|
|
25
|
+
kind: "genericItem";
|
|
26
|
+
customType?: string;
|
|
27
|
+
type?: string;
|
|
28
|
+
element?: string;
|
|
29
|
+
body?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface QaItem {
|
|
32
|
+
kind: "qaItem";
|
|
33
|
+
question: string;
|
|
34
|
+
answer: string;
|
|
35
|
+
explanation?: string;
|
|
36
|
+
difficulty?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface BookChapter {
|
|
39
|
+
kind: "bookChapter";
|
|
40
|
+
chapterTitle: string;
|
|
41
|
+
text: string;
|
|
42
|
+
chapterNumber?: number;
|
|
43
|
+
summary?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface TermDefinition {
|
|
46
|
+
kind: "termDefinition";
|
|
47
|
+
term: string;
|
|
48
|
+
definition: string;
|
|
49
|
+
example?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface InfoExplain {
|
|
52
|
+
kind: "infoExplain";
|
|
53
|
+
topic: string;
|
|
54
|
+
explanation: string;
|
|
55
|
+
source?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface EpisodeItem {
|
|
58
|
+
kind: "episodeItem";
|
|
59
|
+
episodeNumber?: number;
|
|
60
|
+
seasonNumber?: number;
|
|
61
|
+
episodeTitle: string;
|
|
62
|
+
synopsis?: string;
|
|
63
|
+
duration?: number;
|
|
64
|
+
seriesTitle?: string;
|
|
65
|
+
mediaRef?: string;
|
|
66
|
+
releaseDate?: string;
|
|
67
|
+
body?: string;
|
|
68
|
+
thumbnailBase64?: string;
|
|
69
|
+
}
|
|
70
|
+
export type Content = GenericItem | QaItem | BookChapter | TermDefinition | InfoExplain | EpisodeItem;
|
|
71
|
+
export type AttachmentKind = "image" | "video" | "file";
|
|
72
|
+
export interface Attachment {
|
|
73
|
+
id: string;
|
|
74
|
+
fileName: string;
|
|
75
|
+
mimeType: string;
|
|
76
|
+
kind: AttachmentKind;
|
|
77
|
+
sizeBytes?: number;
|
|
78
|
+
data?: string;
|
|
79
|
+
uri?: string;
|
|
80
|
+
caption?: string;
|
|
81
|
+
}
|
|
82
|
+
export type TemplateLayout = "standard" | "card" | "magazine" | "minimal";
|
|
83
|
+
export interface Template {
|
|
84
|
+
ref?: string;
|
|
85
|
+
primaryColor?: string;
|
|
86
|
+
secondaryColor?: string;
|
|
87
|
+
backgroundColor?: string;
|
|
88
|
+
textColor?: string;
|
|
89
|
+
fontFamily?: string;
|
|
90
|
+
layout?: TemplateLayout | string;
|
|
91
|
+
headerAttachmentRef?: string;
|
|
92
|
+
}
|
|
93
|
+
export type MediaKind = "image" | "video" | "audio" | "subtitle" | "file";
|
|
94
|
+
export interface MediaFile {
|
|
95
|
+
path: string;
|
|
96
|
+
kind: MediaKind;
|
|
97
|
+
label?: string;
|
|
98
|
+
}
|
|
99
|
+
export interface DocumentLoop {
|
|
100
|
+
version: string;
|
|
101
|
+
id: string;
|
|
102
|
+
metadata: Metadata;
|
|
103
|
+
loopLinks: LoopLinks;
|
|
104
|
+
content: Content;
|
|
105
|
+
attachments?: Attachment[];
|
|
106
|
+
template?: Template;
|
|
107
|
+
mediaFolder?: MediaFile[];
|
|
108
|
+
/** اسم الملف الأصلي إن كان معروفاً (يفيد أدوات الحلقة والعارض) */
|
|
109
|
+
sourceFileName?: string;
|
|
110
|
+
}
|
|
111
|
+
export interface DlofTemplatePackage {
|
|
112
|
+
id: string;
|
|
113
|
+
name: string;
|
|
114
|
+
author?: string;
|
|
115
|
+
version: string;
|
|
116
|
+
design: Omit<Template, "ref" | "headerAttachmentRef">;
|
|
117
|
+
}
|
|
118
|
+
export interface DlofPkgMeta {
|
|
119
|
+
id: string;
|
|
120
|
+
title: string;
|
|
121
|
+
domain: string;
|
|
122
|
+
version: string;
|
|
123
|
+
author?: string;
|
|
124
|
+
language?: string;
|
|
125
|
+
createdAt?: string;
|
|
126
|
+
dlofpkg_version: string;
|
|
127
|
+
}
|
|
128
|
+
export interface DlofPkg {
|
|
129
|
+
meta: DlofPkgMeta;
|
|
130
|
+
document: DocumentLoop;
|
|
131
|
+
attachments: Record<string, Buffer>;
|
|
132
|
+
}
|
|
133
|
+
export interface DlofSeries {
|
|
134
|
+
name: string;
|
|
135
|
+
/** خريطة اسم الملف (نسبي داخل السلسلة) -> المستند المحلَّل */
|
|
136
|
+
documents: Record<string, DocumentLoop>;
|
|
137
|
+
/** اسم ملف نقطة انطلاق الحلقة (loopRoot) إن وُجد */
|
|
138
|
+
rootFileName?: string;
|
|
139
|
+
/** ملفات إضافية غير .dlof (خطوط، وسائط، إلخ) -> بايتات خام */
|
|
140
|
+
assets: Record<string, Buffer>;
|
|
141
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderDocumentCard = renderDocumentCard;
|
|
4
|
+
const theme_1 = require("./theme");
|
|
5
|
+
function attachmentSrc(a) {
|
|
6
|
+
if (a.data)
|
|
7
|
+
return `data:${a.mimeType};base64,${a.data}`;
|
|
8
|
+
if (a.uri)
|
|
9
|
+
return a.uri;
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
function renderAttachment(a) {
|
|
13
|
+
const src = attachmentSrc(a);
|
|
14
|
+
if (!src)
|
|
15
|
+
return "";
|
|
16
|
+
const caption = a.caption ? `<div class="tag">${(0, theme_1.escapeHtml)(a.caption)}</div>` : "";
|
|
17
|
+
if (a.kind === "image") {
|
|
18
|
+
return `<div class="attachment"><img src="${src}" alt="${(0, theme_1.escapeHtml)(a.fileName)}" loading="lazy"/>${caption}</div>`;
|
|
19
|
+
}
|
|
20
|
+
if (a.kind === "video") {
|
|
21
|
+
return `<div class="attachment"><video controls src="${src}"></video>${caption}</div>`;
|
|
22
|
+
}
|
|
23
|
+
return `<div class="attachment"><a href="${src}" download="${(0, theme_1.escapeHtml)(a.fileName)}">📎 ${(0, theme_1.escapeHtml)(a.fileName)}</a>${caption}</div>`;
|
|
24
|
+
}
|
|
25
|
+
function contentHtml(doc) {
|
|
26
|
+
const c = doc.content;
|
|
27
|
+
switch (c.kind) {
|
|
28
|
+
case "bookChapter":
|
|
29
|
+
return `
|
|
30
|
+
${c.chapterNumber !== undefined ? `<div class="tag">الفصل ${c.chapterNumber}</div>` : ""}
|
|
31
|
+
<h1>${(0, theme_1.escapeHtml)(c.chapterTitle)}</h1>
|
|
32
|
+
${c.summary ? `<p class="tag">${(0, theme_1.escapeHtml)(c.summary)}</p>` : ""}
|
|
33
|
+
<div class="body-text">${(0, theme_1.escapeHtml)(c.text)}</div>`;
|
|
34
|
+
case "qaItem":
|
|
35
|
+
return `
|
|
36
|
+
<h2>❓ ${(0, theme_1.escapeHtml)(c.question)}</h2>
|
|
37
|
+
<div class="body-text">${(0, theme_1.escapeHtml)(c.answer)}</div>
|
|
38
|
+
${c.explanation ? `<p class="tag">${(0, theme_1.escapeHtml)(c.explanation)}</p>` : ""}
|
|
39
|
+
${c.difficulty ? `<span class="badge">${(0, theme_1.escapeHtml)(c.difficulty)}</span>` : ""}`;
|
|
40
|
+
case "termDefinition":
|
|
41
|
+
return `
|
|
42
|
+
<h1>${(0, theme_1.escapeHtml)(c.term)}</h1>
|
|
43
|
+
<div class="body-text">${(0, theme_1.escapeHtml)(c.definition)}</div>
|
|
44
|
+
${c.example ? `<p class="tag">مثال: ${(0, theme_1.escapeHtml)(c.example)}</p>` : ""}`;
|
|
45
|
+
case "infoExplain":
|
|
46
|
+
return `
|
|
47
|
+
<h1>${(0, theme_1.escapeHtml)(c.topic)}</h1>
|
|
48
|
+
<div class="body-text">${(0, theme_1.escapeHtml)(c.explanation)}</div>
|
|
49
|
+
${c.source ? `<p class="tag">المصدر: ${(0, theme_1.escapeHtml)(c.source)}</p>` : ""}`;
|
|
50
|
+
case "episodeItem":
|
|
51
|
+
return `
|
|
52
|
+
<div class="tag">${c.seriesTitle ? (0, theme_1.escapeHtml)(c.seriesTitle) + " · " : ""}${c.seasonNumber !== undefined ? "الموسم " + c.seasonNumber + " · " : ""}${c.episodeNumber !== undefined ? "الحلقة " + c.episodeNumber : ""}</div>
|
|
53
|
+
<h1>${(0, theme_1.escapeHtml)(c.episodeTitle)}</h1>
|
|
54
|
+
${c.synopsis ? `<p class="tag">${(0, theme_1.escapeHtml)(c.synopsis)}</p>` : ""}
|
|
55
|
+
${c.body ? `<div class="body-text">${(0, theme_1.escapeHtml)(c.body)}</div>` : ""}`;
|
|
56
|
+
case "genericItem":
|
|
57
|
+
default:
|
|
58
|
+
return `
|
|
59
|
+
<h1>${(0, theme_1.escapeHtml)(doc.metadata.title || "مستند")}</h1>
|
|
60
|
+
${c.type ? `<span class="badge">${(0, theme_1.escapeHtml)(c.type)}</span>` : ""}
|
|
61
|
+
${c.element ? `<p class="tag">${(0, theme_1.escapeHtml)(c.element)}</p>` : ""}
|
|
62
|
+
${c.body ? `<div class="body-text">${(0, theme_1.escapeHtml)(c.body)}</div>` : ""}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** يبني كتلة HTML كاملة (بطاقة) لمستند DocumentLoop واحد مع مرفقاته */
|
|
66
|
+
function renderDocumentCard(doc, layout) {
|
|
67
|
+
const tags = (doc.metadata.tags ?? []).map((t) => `<span class="tag">#${(0, theme_1.escapeHtml)(t)}</span>`).join("");
|
|
68
|
+
const meta = `<div class="meta-row">${(0, theme_1.escapeHtml)(doc.metadata.author ?? "")}${doc.metadata.author && doc.metadata.createdAt ? " · " : ""}${doc.metadata.createdAt ? (0, theme_1.escapeHtml)(doc.metadata.createdAt.slice(0, 10)) : ""}</div>`;
|
|
69
|
+
const attachmentsHtml = (doc.attachments ?? []).map(renderAttachment).join("\n");
|
|
70
|
+
return `
|
|
71
|
+
<div class="card ${(0, theme_1.escapeHtml)(layout)}">
|
|
72
|
+
${meta}
|
|
73
|
+
${contentHtml(doc)}
|
|
74
|
+
${tags ? `<div style="margin-top:10px">${tags}</div>` : ""}
|
|
75
|
+
${attachmentsHtml}
|
|
76
|
+
</div>`;
|
|
77
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { DlofPkg, DocumentLoop } from "../types";
|
|
2
|
+
export interface ViewerOptions {
|
|
3
|
+
/** يعرض تحذيرات التحقق أسفل البطاقة إن وُجدت (افتراضي: true) */
|
|
4
|
+
showValidation?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* عارض dlofpkg / dlof: يبني صفحة HTML مستقلة واحدة (بلا اعتماديات خارجية)
|
|
8
|
+
* تعرض بيانات meta.json الوصفية، محتوى المستند، مرفقاته، وتصميمه المخصّص (template).
|
|
9
|
+
* الصفحة الناتجة قابلة للفتح مباشرة في أي متصفح دون خادم.
|
|
10
|
+
*/
|
|
11
|
+
export declare function renderViewerHtml(input: DlofPkg | DocumentLoop, options?: ViewerOptions): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderViewerHtml = renderViewerHtml;
|
|
4
|
+
const theme_1 = require("./theme");
|
|
5
|
+
const renderContent_1 = require("./renderContent");
|
|
6
|
+
const validate_1 = require("../dlof/validate");
|
|
7
|
+
/**
|
|
8
|
+
* عارض dlofpkg / dlof: يبني صفحة HTML مستقلة واحدة (بلا اعتماديات خارجية)
|
|
9
|
+
* تعرض بيانات meta.json الوصفية، محتوى المستند، مرفقاته، وتصميمه المخصّص (template).
|
|
10
|
+
* الصفحة الناتجة قابلة للفتح مباشرة في أي متصفح دون خادم.
|
|
11
|
+
*/
|
|
12
|
+
function renderViewerHtml(input, options = {}) {
|
|
13
|
+
const doc = "document" in input ? input.document : input;
|
|
14
|
+
const meta = "meta" in input ? input.meta : undefined;
|
|
15
|
+
const theme = (0, theme_1.resolveTheme)(doc.template);
|
|
16
|
+
const validation = (0, validate_1.validateDlof)(doc);
|
|
17
|
+
const showValidation = options.showValidation ?? true;
|
|
18
|
+
const issuesHtml = showValidation && validation.issues.length
|
|
19
|
+
? `<div class="card minimal" style="border:1px dashed ${validation.valid ? "#999" : "#c62828"}">
|
|
20
|
+
<strong>${validation.valid ? "ملاحظات التحقق" : "أخطاء في الملف"}</strong>
|
|
21
|
+
<ul>${validation.issues
|
|
22
|
+
.map((i) => `<li>[${i.level === "error" ? "خطأ" : "تنبيه"}] ${(0, theme_1.escapeHtml)(i.message)}</li>`)
|
|
23
|
+
.join("")}</ul>
|
|
24
|
+
</div>`
|
|
25
|
+
: "";
|
|
26
|
+
const packageInfo = meta
|
|
27
|
+
? `<div class="meta-row">حزمة dlofpkg · الإصدار ${(0, theme_1.escapeHtml)(meta.dlofpkg_version)} · المعرّف: ${(0, theme_1.escapeHtml)(meta.id)}</div>`
|
|
28
|
+
: "";
|
|
29
|
+
return `<!DOCTYPE html>
|
|
30
|
+
<html lang="${(0, theme_1.escapeHtml)(doc.metadata.language ?? "ar")}" dir="rtl">
|
|
31
|
+
<head>
|
|
32
|
+
<meta charset="UTF-8"/>
|
|
33
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
34
|
+
<title>${(0, theme_1.escapeHtml)(doc.metadata.title || "عارض DLoF")}</title>
|
|
35
|
+
<style>${(0, theme_1.baseCss)(theme)}</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<div class="app">
|
|
39
|
+
<div class="badge">DLoF Viewer</div>
|
|
40
|
+
${packageInfo}
|
|
41
|
+
${(0, renderContent_1.renderDocumentCard)(doc, theme.layout)}
|
|
42
|
+
${issuesHtml}
|
|
43
|
+
</div>
|
|
44
|
+
</body>
|
|
45
|
+
</html>`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Template } from "../types";
|
|
2
|
+
export interface ResolvedTheme {
|
|
3
|
+
primaryColor: string;
|
|
4
|
+
secondaryColor: string;
|
|
5
|
+
backgroundColor: string;
|
|
6
|
+
textColor: string;
|
|
7
|
+
fontFamily: string;
|
|
8
|
+
layout: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveTheme(template?: Template): ResolvedTheme;
|
|
11
|
+
/** يبني كتلة CSS أساسية مشتركة بين العارض والمشغّل، بدعم كامل للـ RTL */
|
|
12
|
+
export declare function baseCss(theme: ResolvedTheme): string;
|
|
13
|
+
export declare function escapeHtml(input: string): string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveTheme = resolveTheme;
|
|
4
|
+
exports.baseCss = baseCss;
|
|
5
|
+
exports.escapeHtml = escapeHtml;
|
|
6
|
+
const DEFAULTS = {
|
|
7
|
+
primaryColor: "#6750A4",
|
|
8
|
+
secondaryColor: "#1E88E5",
|
|
9
|
+
backgroundColor: "#FFFFFF",
|
|
10
|
+
textColor: "#212121",
|
|
11
|
+
fontFamily: "sans-serif",
|
|
12
|
+
layout: "standard",
|
|
13
|
+
};
|
|
14
|
+
const FONT_STACKS = {
|
|
15
|
+
serif: "'Amiri', 'Noto Naskh Arabic', Georgia, serif",
|
|
16
|
+
"sans-serif": "'Tajawal', 'IBM Plex Sans Arabic', 'Segoe UI', sans-serif",
|
|
17
|
+
monospace: "'Cascadia Code', 'Courier New', monospace",
|
|
18
|
+
cursive: "'Aref Ruqaa', cursive",
|
|
19
|
+
};
|
|
20
|
+
function resolveTheme(template) {
|
|
21
|
+
const t = template ?? {};
|
|
22
|
+
const fontFamily = t.fontFamily ? FONT_STACKS[t.fontFamily] ?? t.fontFamily : FONT_STACKS["sans-serif"];
|
|
23
|
+
return {
|
|
24
|
+
primaryColor: t.primaryColor ?? DEFAULTS.primaryColor,
|
|
25
|
+
secondaryColor: t.secondaryColor ?? DEFAULTS.secondaryColor,
|
|
26
|
+
backgroundColor: t.backgroundColor ?? DEFAULTS.backgroundColor,
|
|
27
|
+
textColor: t.textColor ?? DEFAULTS.textColor,
|
|
28
|
+
fontFamily,
|
|
29
|
+
layout: t.layout ?? DEFAULTS.layout,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** يبني كتلة CSS أساسية مشتركة بين العارض والمشغّل، بدعم كامل للـ RTL */
|
|
33
|
+
function baseCss(theme) {
|
|
34
|
+
return `
|
|
35
|
+
:root {
|
|
36
|
+
--primary: ${theme.primaryColor};
|
|
37
|
+
--secondary: ${theme.secondaryColor};
|
|
38
|
+
--bg: ${theme.backgroundColor};
|
|
39
|
+
--text: ${theme.textColor};
|
|
40
|
+
--font: ${theme.fontFamily};
|
|
41
|
+
}
|
|
42
|
+
* { box-sizing: border-box; }
|
|
43
|
+
html, body {
|
|
44
|
+
margin: 0; padding: 0;
|
|
45
|
+
background: var(--bg);
|
|
46
|
+
color: var(--text);
|
|
47
|
+
font-family: var(--font);
|
|
48
|
+
direction: rtl;
|
|
49
|
+
}
|
|
50
|
+
body { min-height: 100vh; }
|
|
51
|
+
a { color: var(--secondary); }
|
|
52
|
+
.app {
|
|
53
|
+
max-width: 860px;
|
|
54
|
+
margin: 0 auto;
|
|
55
|
+
padding: 24px 20px 64px;
|
|
56
|
+
}
|
|
57
|
+
.badge {
|
|
58
|
+
display: inline-block;
|
|
59
|
+
background: var(--primary);
|
|
60
|
+
color: #fff;
|
|
61
|
+
border-radius: 999px;
|
|
62
|
+
padding: 2px 12px;
|
|
63
|
+
font-size: 12px;
|
|
64
|
+
margin-inline-end: 6px;
|
|
65
|
+
}
|
|
66
|
+
.card {
|
|
67
|
+
background: color-mix(in srgb, var(--bg) 92%, #000 4%);
|
|
68
|
+
border-radius: 16px;
|
|
69
|
+
padding: 20px 22px;
|
|
70
|
+
box-shadow: 0 1px 3px rgba(0,0,0,.08);
|
|
71
|
+
margin-bottom: 18px;
|
|
72
|
+
}
|
|
73
|
+
.card.magazine { border-inline-start: 6px solid var(--primary); }
|
|
74
|
+
.card.minimal { box-shadow: none; background: transparent; padding: 8px 0; }
|
|
75
|
+
h1 { font-size: 1.6rem; margin: .2em 0; }
|
|
76
|
+
h2 { font-size: 1.2rem; color: var(--primary); }
|
|
77
|
+
.meta-row { font-size: .85rem; opacity: .75; margin-bottom: 12px; }
|
|
78
|
+
.tag { font-size: .75rem; opacity: .7; margin-inline-end: 8px; }
|
|
79
|
+
.attachment img { max-width: 100%; border-radius: 12px; margin: 10px 0; }
|
|
80
|
+
.attachment video, .attachment audio { width: 100%; margin: 10px 0; }
|
|
81
|
+
.footer-nav {
|
|
82
|
+
display: flex; justify-content: space-between; gap: 12px;
|
|
83
|
+
margin-top: 24px;
|
|
84
|
+
}
|
|
85
|
+
.nav-btn {
|
|
86
|
+
flex: 1; text-align: center; padding: 12px 16px;
|
|
87
|
+
background: var(--primary); color: #fff; border-radius: 12px;
|
|
88
|
+
text-decoration: none; font-weight: 600; cursor: pointer; border: none;
|
|
89
|
+
font-family: var(--font); font-size: 1rem;
|
|
90
|
+
}
|
|
91
|
+
.nav-btn.secondary { background: var(--secondary); }
|
|
92
|
+
.nav-btn:disabled { opacity: .4; cursor: not-allowed; }
|
|
93
|
+
.body-text { white-space: pre-wrap; line-height: 1.9; }
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
function escapeHtml(input) {
|
|
97
|
+
return input
|
|
98
|
+
.replace(/&/g, "&")
|
|
99
|
+
.replace(/</g, "<")
|
|
100
|
+
.replace(/>/g, ">")
|
|
101
|
+
.replace(/"/g, """);
|
|
102
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* محلّل XML خفيف بلا اعتمادات خارجية.
|
|
3
|
+
* غير مصمم كمحلل XML عام كامل المواصفة، بل مخصّص لتغطية كل ما تحتاجه صيغة DLoF:
|
|
4
|
+
* عناصر متداخلة، صفات، نص، CDATA، تعليقات، عناصر ذاتية الإغلاق، وترميز UTF-8.
|
|
5
|
+
*/
|
|
6
|
+
export interface XmlNode {
|
|
7
|
+
tag: string;
|
|
8
|
+
attrs: Record<string, string>;
|
|
9
|
+
children: XmlNode[];
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function encodeEntities(input: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* يحلّل مستند XML كاملاً ويعيد العنصر الجذر.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseXml(source: string): XmlNode;
|
|
17
|
+
/** يبحث عن أول عنصر فرعي مباشر بالاسم المحدد */
|
|
18
|
+
export declare function child(node: XmlNode | undefined, tag: string): XmlNode | undefined;
|
|
19
|
+
/** يعيد كل العناصر الفرعية المباشرة بالاسم المحدد */
|
|
20
|
+
export declare function children(node: XmlNode | undefined, tag: string): XmlNode[];
|
|
21
|
+
/** نص عنصر فرعي مباشر، أو undefined إن لم يوجد */
|
|
22
|
+
export declare function childText(node: XmlNode | undefined, tag: string): string | undefined;
|