@things-factory/help 10.1.234 → 10.1.244
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/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/help-index.d.ts +92 -0
- package/dist-server/help-index.js +212 -0
- package/dist-server/help-index.js.map +1 -0
- package/dist-server/help-store.d.ts +12 -0
- package/dist-server/help-store.js +65 -0
- package/dist-server/help-store.js.map +1 -0
- package/dist-server/index.d.ts +2 -0
- package/dist-server/index.js +7 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/routers/help-router.js +17 -0
- package/dist-server/routers/help-router.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export interface HelpPart {
|
|
2
|
+
/** `office/guide-employee` — the path under /helps without the language and the extension. */
|
|
3
|
+
topic: string;
|
|
4
|
+
/** The heading's id; '' for the part before the first heading. */
|
|
5
|
+
section: string;
|
|
6
|
+
/** The language of the file (`ko`), or '' for the file with no language in its name (the fallback). */
|
|
7
|
+
lang: string;
|
|
8
|
+
/** The heading's words; for section '' the document's title. */
|
|
9
|
+
title: string;
|
|
10
|
+
/** The document's title, so a part reads as 「사용 안내 › 휴가 내기」. */
|
|
11
|
+
documentTitle: string;
|
|
12
|
+
/** The first sentence of the part — what a list shows before the whole is read. */
|
|
13
|
+
summary: string;
|
|
14
|
+
/** The words of the part, tags taken out. */
|
|
15
|
+
text: string;
|
|
16
|
+
}
|
|
17
|
+
export interface HelpHit {
|
|
18
|
+
topic: string;
|
|
19
|
+
section: string;
|
|
20
|
+
title: string;
|
|
21
|
+
documentTitle: string;
|
|
22
|
+
summary: string;
|
|
23
|
+
score: number;
|
|
24
|
+
/** The share of the question's words the part holds (0-1). */
|
|
25
|
+
coverage: number;
|
|
26
|
+
}
|
|
27
|
+
export interface HelpSearchOptions {
|
|
28
|
+
/** The reader's language — a topic is read in it, else in its base (`ko` of `ko-KR`), else the fallback file. */
|
|
29
|
+
lang?: string;
|
|
30
|
+
/** Only topics under this path (`office`). */
|
|
31
|
+
scope?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Who reads (`finance`). A document written for them — its last name ends `-finance` (`office/guide-finance`) — weighs
|
|
34
|
+
* more, so the same screen opens the part of that reader's work.
|
|
35
|
+
*/
|
|
36
|
+
audience?: string;
|
|
37
|
+
limit?: number;
|
|
38
|
+
/**
|
|
39
|
+
* The share of the question's words a part must hold — 0.5 unless named. Pairs of letters are many, so a question of
|
|
40
|
+
* words no document has (「zzzz 없는말」) still meets some of them (「없는」); a part holding half the words is about it.
|
|
41
|
+
*/
|
|
42
|
+
minCoverage?: number;
|
|
43
|
+
}
|
|
44
|
+
/** The words of a piece of html: blocks that draw rather than say are dropped, an image keeps its alt. */
|
|
45
|
+
export declare function textOfHtml(html: string): string;
|
|
46
|
+
type Cut = {
|
|
47
|
+
section: string;
|
|
48
|
+
title: string;
|
|
49
|
+
text: string;
|
|
50
|
+
};
|
|
51
|
+
/** The parts of an html document, and its title. */
|
|
52
|
+
export declare function partsOfHtml(html: string): {
|
|
53
|
+
title: string;
|
|
54
|
+
parts: Cut[];
|
|
55
|
+
};
|
|
56
|
+
/** The parts of a markdown document. Markdown headings carry no id, so its parts are found but opened as the whole. */
|
|
57
|
+
export declare function partsOfMarkdown(markdown: string): {
|
|
58
|
+
title: string;
|
|
59
|
+
parts: Cut[];
|
|
60
|
+
};
|
|
61
|
+
/** `office/guide.ko.html` → topic `office/guide`, lang `ko`. */
|
|
62
|
+
export declare function topicOfFile(path: string): {
|
|
63
|
+
topic: string;
|
|
64
|
+
lang: string;
|
|
65
|
+
kind: 'html' | 'md';
|
|
66
|
+
} | null;
|
|
67
|
+
/** One document's parts, ready for the index. */
|
|
68
|
+
export declare function partsOfDocument(path: string, content: string): HelpPart[];
|
|
69
|
+
/** The words of a text as the index counts them: a Latin word whole, a run of Hangul or Han as its letter pairs. */
|
|
70
|
+
export declare function tokens(text: string): string[];
|
|
71
|
+
export declare class HelpIndex {
|
|
72
|
+
private entries;
|
|
73
|
+
private df;
|
|
74
|
+
private averageLength;
|
|
75
|
+
constructor(parts: HelpPart[]);
|
|
76
|
+
get size(): number;
|
|
77
|
+
/** The language a topic is read in: the reader's, its base, else the file with none in its name — as /helps serves it. */
|
|
78
|
+
private languageOf;
|
|
79
|
+
private readable;
|
|
80
|
+
search(query: string, options?: HelpSearchOptions): HelpHit[];
|
|
81
|
+
/** One part in the reader's language — or the whole document when no section is named. */
|
|
82
|
+
read(topic: string, section?: string, lang?: string): {
|
|
83
|
+
topic: string;
|
|
84
|
+
section: string;
|
|
85
|
+
title: string;
|
|
86
|
+
documentTitle: string;
|
|
87
|
+
text: string;
|
|
88
|
+
} | null;
|
|
89
|
+
/** Whether a topic is written, and its section when one is named — in any language. */
|
|
90
|
+
holds(topic: string, section?: string): boolean;
|
|
91
|
+
}
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* The help documents, cut into the parts a reader is sent to — the one index the screen's `?` and the AI assistant both
|
|
4
|
+
* ask (ADR-0181). Nothing links a screen to a part by hand: the part is found. Every document under /helps counts, html
|
|
5
|
+
* and markdown, in every language it is written in.
|
|
6
|
+
*
|
|
7
|
+
* A part is a heading with an id (`<h2 id="leave">`) and what follows it up to the next heading of the same depth or
|
|
8
|
+
* higher — the same cut `ox-doc section=` shows. The text before the first such heading is the document's own part
|
|
9
|
+
* (section ''). Pure: the store reads the files, this reads text.
|
|
10
|
+
*
|
|
11
|
+
* Korean is not cut at spaces — 「지출결의를」 and 「지출결의」 must meet — so words are cut into pairs of letters (bigrams)
|
|
12
|
+
* and ranked with BM25. A Latin word stays whole.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.HelpIndex = void 0;
|
|
16
|
+
exports.textOfHtml = textOfHtml;
|
|
17
|
+
exports.partsOfHtml = partsOfHtml;
|
|
18
|
+
exports.partsOfMarkdown = partsOfMarkdown;
|
|
19
|
+
exports.topicOfFile = topicOfFile;
|
|
20
|
+
exports.partsOfDocument = partsOfDocument;
|
|
21
|
+
exports.tokens = tokens;
|
|
22
|
+
const BLOCKS = /<(script|style|svg|template|math)\b[\s\S]*?<\/\1>/gi;
|
|
23
|
+
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ', middot: '·', rsaquo: '›', lsaquo: '‹', rarr: '→', larr: '←', hellip: '…', ndash: '–', mdash: '—' };
|
|
24
|
+
function decode(text) {
|
|
25
|
+
return text.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (whole, name) => {
|
|
26
|
+
if (name[0] === '#') {
|
|
27
|
+
const code = name[1] === 'x' || name[1] === 'X' ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10);
|
|
28
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : whole;
|
|
29
|
+
}
|
|
30
|
+
return ENTITIES[name.toLowerCase()] ?? whole;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** The words of a piece of html: blocks that draw rather than say are dropped, an image keeps its alt. */
|
|
34
|
+
function textOfHtml(html) {
|
|
35
|
+
return decode(html
|
|
36
|
+
.replace(BLOCKS, ' ')
|
|
37
|
+
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
38
|
+
.replace(/<img\b[^>]*\balt="([^"]*)"[^>]*>/gi, ' $1 ')
|
|
39
|
+
.replace(/<[^>]+>/g, ' '))
|
|
40
|
+
.replace(/\s+/g, ' ')
|
|
41
|
+
.trim();
|
|
42
|
+
}
|
|
43
|
+
function firstSentence(text) {
|
|
44
|
+
const cut = text.search(/(?<=[.!?。])\s/);
|
|
45
|
+
const one = cut > 0 ? text.slice(0, cut) : text;
|
|
46
|
+
return one.length > 160 ? one.slice(0, 157) + '…' : one;
|
|
47
|
+
}
|
|
48
|
+
/** The parts of an html document, and its title. */
|
|
49
|
+
function partsOfHtml(html) {
|
|
50
|
+
const body = html.replace(BLOCKS, ' ');
|
|
51
|
+
const h1 = /<h1\b[^>]*>([\s\S]*?)<\/h1>/i.exec(body);
|
|
52
|
+
const title = h1 ? textOfHtml(h1[1]) : '';
|
|
53
|
+
const heading = /<h([2-4])\b[^>]*\bid="([^"]+)"[^>]*>([\s\S]*?)<\/h\1>/gi;
|
|
54
|
+
const marks = [...body.matchAll(heading)].map(one => ({ depth: Number(one[1]), id: one[2], title: textOfHtml(one[3]), at: one.index, end: one.index + one[0].length }));
|
|
55
|
+
const parts = [];
|
|
56
|
+
const head = textOfHtml(body.slice(0, marks[0]?.at ?? body.length));
|
|
57
|
+
if (head)
|
|
58
|
+
parts.push({ section: '', title, text: head });
|
|
59
|
+
marks.forEach((mark, i) => {
|
|
60
|
+
const next = marks.slice(i + 1).find(one => one.depth <= mark.depth);
|
|
61
|
+
parts.push({ section: mark.id, title: mark.title, text: textOfHtml(body.slice(mark.end, next?.at ?? body.length)) });
|
|
62
|
+
});
|
|
63
|
+
return { title, parts };
|
|
64
|
+
}
|
|
65
|
+
/** The parts of a markdown document. Markdown headings carry no id, so its parts are found but opened as the whole. */
|
|
66
|
+
function partsOfMarkdown(markdown) {
|
|
67
|
+
const lines = markdown.split('\n');
|
|
68
|
+
const title = lines.find(line => /^#\s/.test(line))?.replace(/^#\s+/, '').trim() ?? '';
|
|
69
|
+
const parts = [];
|
|
70
|
+
let current = { section: '', title, text: '' };
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
const heading = /^(#{2,4})\s+(.*)$/.exec(line);
|
|
73
|
+
if (heading) {
|
|
74
|
+
parts.push(current);
|
|
75
|
+
current = { section: '', title: heading[2].trim(), text: '' };
|
|
76
|
+
}
|
|
77
|
+
else if (!/^#\s/.test(line))
|
|
78
|
+
current.text += line + '\n';
|
|
79
|
+
}
|
|
80
|
+
parts.push(current);
|
|
81
|
+
return {
|
|
82
|
+
title,
|
|
83
|
+
parts: parts
|
|
84
|
+
.map(part => ({ ...part, text: part.text.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1').replace(/\[([^\]]*)\]\([^)]*\)/g, '$1').replace(/[*_`>|#-]+/g, ' ').replace(/\s+/g, ' ').trim() }))
|
|
85
|
+
.filter(part => part.text)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** `office/guide.ko.html` → topic `office/guide`, lang `ko`. */
|
|
89
|
+
function topicOfFile(path) {
|
|
90
|
+
const match = /^\/?(.+?)(?:\.([a-z]{2}(?:-[A-Za-z]{2,4})?))?\.(html|md)$/.exec(path);
|
|
91
|
+
if (!match)
|
|
92
|
+
return null;
|
|
93
|
+
return { topic: match[1], lang: match[2] ?? '', kind: match[3] };
|
|
94
|
+
}
|
|
95
|
+
/** One document's parts, ready for the index. */
|
|
96
|
+
function partsOfDocument(path, content) {
|
|
97
|
+
const file = topicOfFile(path);
|
|
98
|
+
if (!file)
|
|
99
|
+
return [];
|
|
100
|
+
const { title, parts } = file.kind === 'html' ? partsOfHtml(content) : partsOfMarkdown(content);
|
|
101
|
+
const documentTitle = title || file.topic.split('/').pop();
|
|
102
|
+
return parts.map(part => ({
|
|
103
|
+
topic: file.topic,
|
|
104
|
+
section: part.section,
|
|
105
|
+
lang: file.lang,
|
|
106
|
+
title: part.title || documentTitle,
|
|
107
|
+
documentTitle,
|
|
108
|
+
summary: firstSentence(part.text),
|
|
109
|
+
text: part.text
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
/** The words of a text as the index counts them: a Latin word whole, a run of Hangul or Han as its letter pairs. */
|
|
113
|
+
function tokens(text) {
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const run of text.toLowerCase().match(/[\p{Script=Hangul}\p{Script=Han}]+|[\p{L}\p{N}]+/gu) ?? []) {
|
|
116
|
+
if (/^[\p{Script=Hangul}\p{Script=Han}]+$/u.test(run)) {
|
|
117
|
+
if (run.length === 1)
|
|
118
|
+
out.push(run);
|
|
119
|
+
for (let i = 0; i < run.length - 1; i++)
|
|
120
|
+
out.push(run.slice(i, i + 2));
|
|
121
|
+
}
|
|
122
|
+
else if (run.length > 1 || /\p{N}/u.test(run))
|
|
123
|
+
out.push(run);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
const K1 = 1.2;
|
|
128
|
+
const B = 0.75;
|
|
129
|
+
/* A title is what the part is about; its words count as much as three times the body's. */
|
|
130
|
+
const TITLE_WEIGHT = 3;
|
|
131
|
+
const AUDIENCE_WEIGHT = 2;
|
|
132
|
+
class HelpIndex {
|
|
133
|
+
constructor(parts) {
|
|
134
|
+
this.df = new Map();
|
|
135
|
+
this.entries = parts.map(part => {
|
|
136
|
+
const counts = new Map();
|
|
137
|
+
const add = (words, weight) => words.forEach(word => counts.set(word, (counts.get(word) ?? 0) + weight));
|
|
138
|
+
add(tokens(part.title), TITLE_WEIGHT);
|
|
139
|
+
add(tokens(part.text), 1);
|
|
140
|
+
return { part, counts, length: [...counts.values()].reduce((sum, n) => sum + n, 0) };
|
|
141
|
+
});
|
|
142
|
+
for (const entry of this.entries)
|
|
143
|
+
for (const word of entry.counts.keys())
|
|
144
|
+
this.df.set(word, (this.df.get(word) ?? 0) + 1);
|
|
145
|
+
this.averageLength = this.entries.reduce((sum, one) => sum + one.length, 0) / (this.entries.length || 1);
|
|
146
|
+
}
|
|
147
|
+
get size() {
|
|
148
|
+
return this.entries.length;
|
|
149
|
+
}
|
|
150
|
+
/** The language a topic is read in: the reader's, its base, else the file with none in its name — as /helps serves it. */
|
|
151
|
+
languageOf(topic, lang = '') {
|
|
152
|
+
const written = new Set(this.entries.filter(one => one.part.topic === topic).map(one => one.part.lang));
|
|
153
|
+
return [lang, lang.split('-')[0], ''].find(one => written.has(one)) ?? [...written][0];
|
|
154
|
+
}
|
|
155
|
+
readable(lang) {
|
|
156
|
+
const chosen = new Map();
|
|
157
|
+
return this.entries.filter(entry => {
|
|
158
|
+
const { topic } = entry.part;
|
|
159
|
+
if (!chosen.has(topic))
|
|
160
|
+
chosen.set(topic, this.languageOf(topic, lang));
|
|
161
|
+
return entry.part.lang === chosen.get(topic);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
search(query, options = {}) {
|
|
165
|
+
const words = [...new Set(tokens(query))];
|
|
166
|
+
if (!words.length)
|
|
167
|
+
return [];
|
|
168
|
+
const n = this.entries.length;
|
|
169
|
+
const scope = options.scope?.replace(/\/+$/, '');
|
|
170
|
+
const hits = [];
|
|
171
|
+
for (const entry of this.readable(options.lang)) {
|
|
172
|
+
const { part } = entry;
|
|
173
|
+
if (scope && part.topic !== scope && !part.topic.startsWith(scope + '/'))
|
|
174
|
+
continue;
|
|
175
|
+
let score = 0;
|
|
176
|
+
let held = 0;
|
|
177
|
+
for (const word of words) {
|
|
178
|
+
const tf = entry.counts.get(word);
|
|
179
|
+
if (!tf)
|
|
180
|
+
continue;
|
|
181
|
+
held++;
|
|
182
|
+
const df = this.df.get(word) ?? 0;
|
|
183
|
+
const idf = Math.log(1 + (n - df + 0.5) / (df + 0.5));
|
|
184
|
+
score += (idf * tf * (K1 + 1)) / (tf + K1 * (1 - B + (B * entry.length) / this.averageLength));
|
|
185
|
+
}
|
|
186
|
+
const coverage = held / words.length;
|
|
187
|
+
if (!score || coverage < (options.minCoverage ?? 0.5))
|
|
188
|
+
continue;
|
|
189
|
+
if (options.audience && part.topic.split('/').pop().endsWith(`-${options.audience}`))
|
|
190
|
+
score *= AUDIENCE_WEIGHT;
|
|
191
|
+
hits.push({ topic: part.topic, section: part.section, title: part.title, documentTitle: part.documentTitle, summary: part.summary, score: Math.round(score * 1000) / 1000, coverage: Math.round(coverage * 100) / 100 });
|
|
192
|
+
}
|
|
193
|
+
return hits.sort((a, b) => b.score - a.score).slice(0, options.limit ?? 5);
|
|
194
|
+
}
|
|
195
|
+
/** One part in the reader's language — or the whole document when no section is named. */
|
|
196
|
+
read(topic, section = '', lang) {
|
|
197
|
+
const chosen = this.languageOf(topic, lang);
|
|
198
|
+
const parts = this.entries.filter(one => one.part.topic === topic && one.part.lang === chosen).map(one => one.part);
|
|
199
|
+
if (!parts.length)
|
|
200
|
+
return null;
|
|
201
|
+
if (!section)
|
|
202
|
+
return { topic, section: '', title: parts[0].documentTitle, documentTitle: parts[0].documentTitle, text: parts.map(one => (one.section ? `## ${one.title}\n${one.text}` : one.text)).join('\n\n') };
|
|
203
|
+
const part = parts.find(one => one.section === section);
|
|
204
|
+
return part ? { topic, section, title: part.title, documentTitle: part.documentTitle, text: part.text } : null;
|
|
205
|
+
}
|
|
206
|
+
/** Whether a topic is written, and its section when one is named — in any language. */
|
|
207
|
+
holds(topic, section = '') {
|
|
208
|
+
return this.entries.some(one => one.part.topic === topic && (!section || one.part.section === section));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
exports.HelpIndex = HelpIndex;
|
|
212
|
+
//# sourceMappingURL=help-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"help-index.js","sourceRoot":"","sources":["../server/help-index.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The help documents, cut into the parts a reader is sent to — the one index the screen's `?` and the AI assistant both\n * ask (ADR-0181). Nothing links a screen to a part by hand: the part is found. Every document under /helps counts, html\n * and markdown, in every language it is written in.\n *\n * A part is a heading with an id (`<h2 id=\"leave\">`) and what follows it up to the next heading of the same depth or\n * higher — the same cut `ox-doc section=` shows. The text before the first such heading is the document's own part\n * (section ''). Pure: the store reads the files, this reads text.\n *\n * Korean is not cut at spaces — 「지출결의를」 and 「지출결의」 must meet — so words are cut into pairs of letters (bigrams)\n * and ranked with BM25. A Latin word stays whole.\n */\n\nexport interface HelpPart {\n /** `office/guide-employee` — the path under /helps without the language and the extension. */\n topic: string\n /** The heading's id; '' for the part before the first heading. */\n section: string\n /** The language of the file (`ko`), or '' for the file with no language in its name (the fallback). */\n lang: string\n /** The heading's words; for section '' the document's title. */\n title: string\n /** The document's title, so a part reads as 「사용 안내 › 휴가 내기」. */\n documentTitle: string\n /** The first sentence of the part — what a list shows before the whole is read. */\n summary: string\n /** The words of the part, tags taken out. */\n text: string\n}\n\nexport interface HelpHit {\n topic: string\n section: string\n title: string\n documentTitle: string\n summary: string\n score: number\n /** The share of the question's words the part holds (0-1). */\n coverage: number\n}\n\nexport interface HelpSearchOptions {\n /** The reader's language — a topic is read in it, else in its base (`ko` of `ko-KR`), else the fallback file. */\n lang?: string\n /** Only topics under this path (`office`). */\n scope?: string\n /**\n * Who reads (`finance`). A document written for them — its last name ends `-finance` (`office/guide-finance`) — weighs\n * more, so the same screen opens the part of that reader's work.\n */\n audience?: string\n limit?: number\n /**\n * The share of the question's words a part must hold — 0.5 unless named. Pairs of letters are many, so a question of\n * words no document has (「zzzz 없는말」) still meets some of them (「없는」); a part holding half the words is about it.\n */\n minCoverage?: number\n}\n\nconst BLOCKS = /<(script|style|svg|template|math)\\b[\\s\\S]*?<\\/\\1>/gi\nconst ENTITIES: Record<string, string> = { amp: '&', lt: '<', gt: '>', quot: '\"', apos: \"'\", nbsp: ' ', middot: '·', rsaquo: '›', lsaquo: '‹', rarr: '→', larr: '←', hellip: '…', ndash: '–', mdash: '—' }\n\nfunction decode(text: string): string {\n return text.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (whole, name: string) => {\n if (name[0] === '#') {\n const code = name[1] === 'x' || name[1] === 'X' ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10)\n return Number.isFinite(code) ? String.fromCodePoint(code) : whole\n }\n return ENTITIES[name.toLowerCase()] ?? whole\n })\n}\n\n/** The words of a piece of html: blocks that draw rather than say are dropped, an image keeps its alt. */\nexport function textOfHtml(html: string): string {\n return decode(\n html\n .replace(BLOCKS, ' ')\n .replace(/<!--[\\s\\S]*?-->/g, ' ')\n .replace(/<img\\b[^>]*\\balt=\"([^\"]*)\"[^>]*>/gi, ' $1 ')\n .replace(/<[^>]+>/g, ' ')\n )\n .replace(/\\s+/g, ' ')\n .trim()\n}\n\nfunction firstSentence(text: string): string {\n const cut = text.search(/(?<=[.!?。])\\s/)\n const one = cut > 0 ? text.slice(0, cut) : text\n return one.length > 160 ? one.slice(0, 157) + '…' : one\n}\n\ntype Cut = { section: string; title: string; text: string }\n\n/** The parts of an html document, and its title. */\nexport function partsOfHtml(html: string): { title: string; parts: Cut[] } {\n const body = html.replace(BLOCKS, ' ')\n const h1 = /<h1\\b[^>]*>([\\s\\S]*?)<\\/h1>/i.exec(body)\n const title = h1 ? textOfHtml(h1[1]) : ''\n const heading = /<h([2-4])\\b[^>]*\\bid=\"([^\"]+)\"[^>]*>([\\s\\S]*?)<\\/h\\1>/gi\n const marks = [...body.matchAll(heading)].map(one => ({ depth: Number(one[1]), id: one[2], title: textOfHtml(one[3]), at: one.index!, end: one.index! + one[0].length }))\n const parts: Cut[] = []\n const head = textOfHtml(body.slice(0, marks[0]?.at ?? body.length))\n if (head) parts.push({ section: '', title, text: head })\n marks.forEach((mark, i) => {\n const next = marks.slice(i + 1).find(one => one.depth <= mark.depth)\n parts.push({ section: mark.id, title: mark.title, text: textOfHtml(body.slice(mark.end, next?.at ?? body.length)) })\n })\n return { title, parts }\n}\n\n/** The parts of a markdown document. Markdown headings carry no id, so its parts are found but opened as the whole. */\nexport function partsOfMarkdown(markdown: string): { title: string; parts: Cut[] } {\n const lines = markdown.split('\\n')\n const title = lines.find(line => /^#\\s/.test(line))?.replace(/^#\\s+/, '').trim() ?? ''\n const parts: Cut[] = []\n let current: Cut = { section: '', title, text: '' }\n for (const line of lines) {\n const heading = /^(#{2,4})\\s+(.*)$/.exec(line)\n if (heading) {\n parts.push(current)\n current = { section: '', title: heading[2].trim(), text: '' }\n } else if (!/^#\\s/.test(line)) current.text += line + '\\n'\n }\n parts.push(current)\n return {\n title,\n parts: parts\n .map(part => ({ ...part, text: part.text.replace(/!\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1').replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, '$1').replace(/[*_`>|#-]+/g, ' ').replace(/\\s+/g, ' ').trim() }))\n .filter(part => part.text)\n }\n}\n\n/** `office/guide.ko.html` → topic `office/guide`, lang `ko`. */\nexport function topicOfFile(path: string): { topic: string; lang: string; kind: 'html' | 'md' } | null {\n const match = /^\\/?(.+?)(?:\\.([a-z]{2}(?:-[A-Za-z]{2,4})?))?\\.(html|md)$/.exec(path)\n if (!match) return null\n return { topic: match[1], lang: match[2] ?? '', kind: match[3] as 'html' | 'md' }\n}\n\n/** One document's parts, ready for the index. */\nexport function partsOfDocument(path: string, content: string): HelpPart[] {\n const file = topicOfFile(path)\n if (!file) return []\n const { title, parts } = file.kind === 'html' ? partsOfHtml(content) : partsOfMarkdown(content)\n const documentTitle = title || file.topic.split('/').pop()!\n return parts.map(part => ({\n topic: file.topic,\n section: part.section,\n lang: file.lang,\n title: part.title || documentTitle,\n documentTitle,\n summary: firstSentence(part.text),\n text: part.text\n }))\n}\n\n/** The words of a text as the index counts them: a Latin word whole, a run of Hangul or Han as its letter pairs. */\nexport function tokens(text: string): string[] {\n const out: string[] = []\n for (const run of text.toLowerCase().match(/[\\p{Script=Hangul}\\p{Script=Han}]+|[\\p{L}\\p{N}]+/gu) ?? []) {\n if (/^[\\p{Script=Hangul}\\p{Script=Han}]+$/u.test(run)) {\n if (run.length === 1) out.push(run)\n for (let i = 0; i < run.length - 1; i++) out.push(run.slice(i, i + 2))\n } else if (run.length > 1 || /\\p{N}/u.test(run)) out.push(run)\n }\n return out\n}\n\nconst K1 = 1.2\nconst B = 0.75\n/* A title is what the part is about; its words count as much as three times the body's. */\nconst TITLE_WEIGHT = 3\nconst AUDIENCE_WEIGHT = 2\n\ntype Entry = { part: HelpPart; counts: Map<string, number>; length: number }\n\nexport class HelpIndex {\n private entries: Entry[]\n private df = new Map<string, number>()\n private averageLength: number\n\n constructor(parts: HelpPart[]) {\n this.entries = parts.map(part => {\n const counts = new Map<string, number>()\n const add = (words: string[], weight: number) => words.forEach(word => counts.set(word, (counts.get(word) ?? 0) + weight))\n add(tokens(part.title), TITLE_WEIGHT)\n add(tokens(part.text), 1)\n return { part, counts, length: [...counts.values()].reduce((sum, n) => sum + n, 0) }\n })\n for (const entry of this.entries) for (const word of entry.counts.keys()) this.df.set(word, (this.df.get(word) ?? 0) + 1)\n this.averageLength = this.entries.reduce((sum, one) => sum + one.length, 0) / (this.entries.length || 1)\n }\n\n get size(): number {\n return this.entries.length\n }\n\n /** The language a topic is read in: the reader's, its base, else the file with none in its name — as /helps serves it. */\n private languageOf(topic: string, lang = ''): string | undefined {\n const written = new Set(this.entries.filter(one => one.part.topic === topic).map(one => one.part.lang))\n return [lang, lang.split('-')[0], ''].find(one => written.has(one)) ?? [...written][0]\n }\n\n private readable(lang?: string): Entry[] {\n const chosen = new Map<string, string | undefined>()\n return this.entries.filter(entry => {\n const { topic } = entry.part\n if (!chosen.has(topic)) chosen.set(topic, this.languageOf(topic, lang))\n return entry.part.lang === chosen.get(topic)\n })\n }\n\n search(query: string, options: HelpSearchOptions = {}): HelpHit[] {\n const words = [...new Set(tokens(query))]\n if (!words.length) return []\n const n = this.entries.length\n const scope = options.scope?.replace(/\\/+$/, '')\n const hits: HelpHit[] = []\n for (const entry of this.readable(options.lang)) {\n const { part } = entry\n if (scope && part.topic !== scope && !part.topic.startsWith(scope + '/')) continue\n let score = 0\n let held = 0\n for (const word of words) {\n const tf = entry.counts.get(word)\n if (!tf) continue\n held++\n const df = this.df.get(word) ?? 0\n const idf = Math.log(1 + (n - df + 0.5) / (df + 0.5))\n score += (idf * tf * (K1 + 1)) / (tf + K1 * (1 - B + (B * entry.length) / this.averageLength))\n }\n const coverage = held / words.length\n if (!score || coverage < (options.minCoverage ?? 0.5)) continue\n if (options.audience && part.topic.split('/').pop()!.endsWith(`-${options.audience}`)) score *= AUDIENCE_WEIGHT\n hits.push({ topic: part.topic, section: part.section, title: part.title, documentTitle: part.documentTitle, summary: part.summary, score: Math.round(score * 1000) / 1000, coverage: Math.round(coverage * 100) / 100 })\n }\n return hits.sort((a, b) => b.score - a.score).slice(0, options.limit ?? 5)\n }\n\n /** One part in the reader's language — or the whole document when no section is named. */\n read(topic: string, section = '', lang?: string): { topic: string; section: string; title: string; documentTitle: string; text: string } | null {\n const chosen = this.languageOf(topic, lang)\n const parts = this.entries.filter(one => one.part.topic === topic && one.part.lang === chosen).map(one => one.part)\n if (!parts.length) return null\n if (!section) return { topic, section: '', title: parts[0].documentTitle, documentTitle: parts[0].documentTitle, text: parts.map(one => (one.section ? `## ${one.title}\\n${one.text}` : one.text)).join('\\n\\n') }\n const part = parts.find(one => one.section === section)\n return part ? { topic, section, title: part.title, documentTitle: part.documentTitle, text: part.text } : null\n }\n\n /** Whether a topic is written, and its section when one is named — in any language. */\n holds(topic: string, section = ''): boolean {\n return this.entries.some(one => one.part.topic === topic && (!section || one.part.section === section))\n }\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HelpIndex, type HelpHit, type HelpSearchOptions } from './help-index.js';
|
|
2
|
+
export declare function helpIndex(): HelpIndex;
|
|
3
|
+
/** The parts of the help that answer a question — the screen's `?` and the AI assistant ask the same. */
|
|
4
|
+
export declare function searchHelp(query: string, options?: HelpSearchOptions): HelpHit[];
|
|
5
|
+
/** One part of the help in the reader's language, or the whole document when no section is named. */
|
|
6
|
+
export declare function readHelp(topic: string, section?: string, lang?: string): {
|
|
7
|
+
topic: string;
|
|
8
|
+
section: string;
|
|
9
|
+
title: string;
|
|
10
|
+
documentTitle: string;
|
|
11
|
+
text: string;
|
|
12
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.helpIndex = helpIndex;
|
|
4
|
+
exports.searchHelp = searchHelp;
|
|
5
|
+
exports.readHelp = readHelp;
|
|
6
|
+
const tslib_1 = require("tslib");
|
|
7
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
8
|
+
const help_index_js_1 = require("./help-index.js");
|
|
9
|
+
/*
|
|
10
|
+
* The index of the help documents the application serves — read from where /helps is served (dist-app/helps), once, and
|
|
11
|
+
* again whenever the client is rebuilt in development (webpack writes to memory then, as help-router reads).
|
|
12
|
+
*/
|
|
13
|
+
var fs = require('fs');
|
|
14
|
+
let index;
|
|
15
|
+
process.on('client-rebuilt', (_app, compilerFileSystem) => {
|
|
16
|
+
fs = compilerFileSystem;
|
|
17
|
+
index = undefined;
|
|
18
|
+
});
|
|
19
|
+
function helpsRoot() {
|
|
20
|
+
return path_1.default.resolve(path_1.default.resolve('.'), 'dist-app', 'helps');
|
|
21
|
+
}
|
|
22
|
+
function files(dir, prefix = '') {
|
|
23
|
+
let names;
|
|
24
|
+
try {
|
|
25
|
+
names = fs.readdirSync(dir);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return names.flatMap(name => {
|
|
31
|
+
const full = path_1.default.join(dir, name);
|
|
32
|
+
const relative = prefix ? `${prefix}/${name}` : name;
|
|
33
|
+
try {
|
|
34
|
+
if (fs.statSync(full).isDirectory())
|
|
35
|
+
return files(full, relative);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return /\.(html|md)$/.test(name) ? [relative] : [];
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function helpIndex() {
|
|
44
|
+
if (!index) {
|
|
45
|
+
const root = helpsRoot();
|
|
46
|
+
index = new help_index_js_1.HelpIndex(files(root).flatMap(file => {
|
|
47
|
+
try {
|
|
48
|
+
return (0, help_index_js_1.partsOfDocument)(file, String(fs.readFileSync(path_1.default.join(root, file))));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
return index;
|
|
56
|
+
}
|
|
57
|
+
/** The parts of the help that answer a question — the screen's `?` and the AI assistant ask the same. */
|
|
58
|
+
function searchHelp(query, options = {}) {
|
|
59
|
+
return helpIndex().search(query, options);
|
|
60
|
+
}
|
|
61
|
+
/** One part of the help in the reader's language, or the whole document when no section is named. */
|
|
62
|
+
function readHelp(topic, section = '', lang) {
|
|
63
|
+
return helpIndex().read(topic, section, lang);
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=help-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"help-store.js","sourceRoot":"","sources":["../server/help-store.ts"],"names":[],"mappings":";;AAuCA,8BAcC;AAGD,gCAEC;AAGD,4BAEC;;AA/DD,wDAAuB;AAEvB,mDAAkG;AAElG;;;GAGG;AACH,IAAI,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AACtB,IAAI,KAA4B,CAAA;AAEhC,OAAO,CAAC,EAAE,CAAC,gBAAuB,EAAE,CAAC,IAAa,EAAE,kBAAuB,EAAE,EAAE;IAC7E,EAAE,GAAG,kBAAkB,CAAA;IACvB,KAAK,GAAG,SAAS,CAAA;AACnB,CAAC,CAAC,CAAA;AAEF,SAAS,SAAS;IAChB,OAAO,cAAI,CAAC,OAAO,CAAC,cAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,CAAA;AAC7D,CAAC;AAED,SAAS,KAAK,CAAC,GAAW,EAAE,MAAM,GAAG,EAAE;IACrC,IAAI,KAAe,CAAA;IACnB,IAAI,CAAC;QACH,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;IACD,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;QAC1B,MAAM,IAAI,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QACjC,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;QACpD,IAAI,CAAC;YACH,IAAI,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE;gBAAE,OAAO,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QACnE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAA;QACX,CAAC;QACD,OAAO,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACpD,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,SAAgB,SAAS;IACvB,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,GAAG,SAAS,EAAE,CAAA;QACxB,KAAK,GAAG,IAAI,yBAAS,CACnB,KAAK,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;YACzB,IAAI,CAAC;gBACH,OAAO,IAAA,+BAAe,EAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,cAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;YAC9E,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,EAAE,CAAA;YACX,CAAC;QACH,CAAC,CAAC,CACH,CAAA;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,yGAAyG;AACzG,SAAgB,UAAU,CAAC,KAAa,EAAE,UAA6B,EAAE;IACvE,OAAO,SAAS,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;AAC3C,CAAC;AAED,qGAAqG;AACrG,SAAgB,QAAQ,CAAC,KAAa,EAAE,OAAO,GAAG,EAAE,EAAE,IAAa;IACjE,OAAO,SAAS,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,CAAA;AAC/C,CAAC","sourcesContent":["import path from 'path'\n\nimport { HelpIndex, partsOfDocument, type HelpHit, type HelpSearchOptions } from './help-index.js'\n\n/*\n * The index of the help documents the application serves — read from where /helps is served (dist-app/helps), once, and\n * again whenever the client is rebuilt in development (webpack writes to memory then, as help-router reads).\n */\nvar fs = require('fs')\nlet index: HelpIndex | undefined\n\nprocess.on('client-rebuilt' as any, (_app: unknown, compilerFileSystem: any) => {\n fs = compilerFileSystem\n index = undefined\n})\n\nfunction helpsRoot(): string {\n return path.resolve(path.resolve('.'), 'dist-app', 'helps')\n}\n\nfunction files(dir: string, prefix = ''): string[] {\n let names: string[]\n try {\n names = fs.readdirSync(dir)\n } catch {\n return []\n }\n return names.flatMap(name => {\n const full = path.join(dir, name)\n const relative = prefix ? `${prefix}/${name}` : name\n try {\n if (fs.statSync(full).isDirectory()) return files(full, relative)\n } catch {\n return []\n }\n return /\\.(html|md)$/.test(name) ? [relative] : []\n })\n}\n\nexport function helpIndex(): HelpIndex {\n if (!index) {\n const root = helpsRoot()\n index = new HelpIndex(\n files(root).flatMap(file => {\n try {\n return partsOfDocument(file, String(fs.readFileSync(path.join(root, file))))\n } catch {\n return []\n }\n })\n )\n }\n return index\n}\n\n/** The parts of the help that answer a question — the screen's `?` and the AI assistant ask the same. */\nexport function searchHelp(query: string, options: HelpSearchOptions = {}): HelpHit[] {\n return helpIndex().search(query, options)\n}\n\n/** One part of the help in the reader's language, or the whole document when no section is named. */\nexport function readHelp(topic: string, section = '', lang?: string) {\n return helpIndex().read(topic, section, lang)\n}\n"]}
|
package/dist-server/index.d.ts
CHANGED
package/dist-server/index.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readHelp = exports.searchHelp = exports.helpIndex = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
3
5
|
require("./routes.js");
|
|
6
|
+
tslib_1.__exportStar(require("./help-index.js"), exports);
|
|
7
|
+
var help_store_js_1 = require("./help-store.js");
|
|
8
|
+
Object.defineProperty(exports, "helpIndex", { enumerable: true, get: function () { return help_store_js_1.helpIndex; } });
|
|
9
|
+
Object.defineProperty(exports, "searchHelp", { enumerable: true, get: function () { return help_store_js_1.searchHelp; } });
|
|
10
|
+
Object.defineProperty(exports, "readHelp", { enumerable: true, get: function () { return help_store_js_1.readHelp; } });
|
|
4
11
|
//# sourceMappingURL=index.js.map
|
package/dist-server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;;AAAA,uBAAoB;AAEpB,0DAA+B;AAC/B,iDAAiE;AAAxD,0GAAA,SAAS,OAAA;AAAE,2GAAA,UAAU,OAAA;AAAE,yGAAA,QAAQ,OAAA","sourcesContent":["import './routes.js'\n\nexport * from './help-index.js'\nexport { helpIndex, searchHelp, readHelp } from './help-store.js'\n"]}
|
|
@@ -4,12 +4,29 @@ exports.helpRouter = void 0;
|
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const koa_router_1 = tslib_1.__importDefault(require("koa-router"));
|
|
6
6
|
const path_1 = tslib_1.__importDefault(require("path"));
|
|
7
|
+
const help_store_js_1 = require("../help-store.js");
|
|
7
8
|
exports.helpRouter = new koa_router_1.default();
|
|
8
9
|
var fs = require('fs');
|
|
9
10
|
process.on('client-rebuilt', (app, compilerFileSystem) => {
|
|
10
11
|
/* change filesystem whenever webpacking assets done. only for development mode. */
|
|
11
12
|
fs = compilerFileSystem;
|
|
12
13
|
});
|
|
14
|
+
/*
|
|
15
|
+
* 도움말 검색(ADR-0181) — 화면의 `?` 가 「이 화면의 도움말」을, AI 비서가 질문의 도움말을 같은 색인에서 찾는다.
|
|
16
|
+
* `q` 는 검색어, `scope` 는 주제 경로(`office`), `audience` 는 읽는 사람(`finance`), 언어는 요청의 것.
|
|
17
|
+
*/
|
|
18
|
+
exports.helpRouter.get('/search', async (context) => {
|
|
19
|
+
const { q = '', scope, audience, limit, coverage } = context.query;
|
|
20
|
+
const minCoverage = Number(coverage);
|
|
21
|
+
context.body = (0, help_store_js_1.searchHelp)(String(q), {
|
|
22
|
+
lang: context.language,
|
|
23
|
+
scope: scope || undefined,
|
|
24
|
+
audience: audience || undefined,
|
|
25
|
+
limit: Math.min(Math.max(Number(limit) || 5, 1), 20),
|
|
26
|
+
minCoverage: minCoverage > 0 && minCoverage <= 1 ? minCoverage : undefined
|
|
27
|
+
});
|
|
28
|
+
context.type = 'application/json; charset=UTF-8';
|
|
29
|
+
});
|
|
13
30
|
exports.helpRouter.get('/(.+)/index.md', async (context, next) => {
|
|
14
31
|
const appRootPath = path_1.default.resolve('.');
|
|
15
32
|
const PUBLIC_ROOT = path_1.default.resolve(appRootPath, 'dist-app'); // webpack config output.path
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"help-router.js","sourceRoot":"","sources":["../../server/routers/help-router.ts"],"names":[],"mappings":";;;;AAAA,oEAA+B;AAC/B,wDAAuB;
|
|
1
|
+
{"version":3,"file":"help-router.js","sourceRoot":"","sources":["../../server/routers/help-router.ts"],"names":[],"mappings":";;;;AAAA,oEAA+B;AAC/B,wDAAuB;AAEvB,oDAA6C;AAEhC,QAAA,UAAU,GAAG,IAAI,oBAAM,EAAE,CAAA;AAEtC,IAAI,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAEtB,OAAO,CAAC,EAAE,CAAC,gBAAuB,EAAE,CAAC,GAAG,EAAE,kBAAkB,EAAE,EAAE;IAC9D,mFAAmF;IACnF,EAAE,GAAG,kBAAkB,CAAA;AACzB,CAAC,CAAC,CAAA;AAEF;;;GAGG;AACH,kBAAU,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,EAAC,OAAO,EAAC,EAAE;IACxC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,KAA2C,CAAA;IACxG,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAA;IACpC,OAAO,CAAC,IAAI,GAAG,IAAA,0BAAU,EAAC,MAAM,CAAC,CAAC,CAAC,EAAE;QACnC,IAAI,EAAG,OAAe,CAAC,QAAQ;QAC/B,KAAK,EAAE,KAAK,IAAI,SAAS;QACzB,QAAQ,EAAE,QAAQ,IAAI,SAAS;QAC/B,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;QACpD,WAAW,EAAE,WAAW,GAAG,CAAC,IAAI,WAAW,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;KAC3E,CAAC,CAAA;IACF,OAAO,CAAC,IAAI,GAAG,iCAAiC,CAAA;AAClD,CAAC,CAAC,CAAA;AAEF,kBAAU,CAAC,GAAG,CAAC,gBAAgB,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IACvD,MAAM,WAAW,GAAG,cAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACrC,MAAM,WAAW,GAAG,cAAI,CAAC,OAAO,CAAC,WAAW,EAAE,UAAU,CAAC,CAAA,CAAC,6BAA6B;IACvF,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAA;IAClC,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAA;IACnE,MAAM,UAAU,GAAG,WAAW,GAAG,MAAM,CAAA;IAEvC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,UAAU,WAAW,CAAC,EAAE,CAAC;QAC5C,OAAO,CAAC,GAAG,GAAG,GAAG,QAAQ,EAAE,CAAA;QAC3B,OAAO,IAAI,EAAE,CAAA;IACf,CAAC;IAED,uDAAuD;IACvD,MAAM,SAAS,GAAG,EAAE,CAAA;IAEpB,+BAA+B;IAC/B,MAAM,cAAc,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,CAAA;IAEjD,iDAAiD;IACjD,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;QAClC,4DAA4D;QAC5D,MAAM,gBAAgB,GAAG,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAA;QACrD,IAAI,gBAAgB,EAAE,CAAC;YACrB,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,gBAAgB,CAAA;YAC1C,SAAS,CAAC,IAAI,CAAC,MAAM,KAAK,KAAK,MAAM,IAAI,KAAK,MAAM,CAAC,CAAA;QACvD,CAAC;aAAM,CAAC;YACN,mDAAmD;YACnD,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;YACzC,IAAI,UAAU,EAAE,CAAC;gBACf,MAAM,CAAC,EAAE,KAAK,CAAC,GAAG,UAAU,CAAA;gBAC5B,SAAS,CAAC,IAAI,CAAC,MAAM,KAAK,KAAK,MAAM,IAAI,KAAK,MAAM,CAAC,CAAA;YACvD,CAAC;QACH,CAAC;IACH,CAAC;IAED,0DAA0D;IAC1D,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,CAAA;QAC1C,MAAM,iBAAiB,GAAG,CAAC,KAAK,MAAM,EAAE,EAAE,GAAG,UAAU,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QACrE,OAAO,CAAC,IAAI,GAAG,iBAAiB,CAAA;QAChC,OAAO,CAAC,IAAI,GAAG,8BAA8B,CAAA;IAC/C,CAAC;SAAM,CAAC;QACN,wEAAwE;QACxE,OAAO,CAAC,IAAI,GAAG,iCAAiC,CAAA;IAClD,CAAC;AACH,CAAC,CAAC,CAAA;AAEF,kBAAU,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IACjD,MAAM,WAAW,GAAG,cAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACrC,MAAM,WAAW,GAAG,cAAI,CAAC,OAAO,CAAC,WAAW,EAAE,UAAU,CAAC,CAAA,CAAC,6BAA6B;IAEvF,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAA;IAErD,MAAM,QAAQ,GAAG,WAAW,GAAG,QAAQ,CAAA;IACvC,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAA;IAC/D,MAAM,aAAa,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAA;IAEpE,IAAI,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC5E,MAAM,WAAW,GAAG,OAAO,EAAE,OAAO,EAAE,WAAW,CAAA;IACjD,IAAI,WAAW,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QACpD,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAC7B,CAAC;IAED,IAAI,GAAG,GAAG,CAAC,QAAQ,EAAE,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,QAAQ,IAAI,GAAG,KAAK,CAAC,CAAC,CAAA;IACtF,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,GAAG,GAAG,aAAa,IAAI,GAAG,KAAK,CAAA;IAC5C,CAAC;IAED,OAAO,IAAI,EAAE,CAAA;AACf,CAAC,CAAC,CAAA;AAEF;;;GAGG;AACH,kBAAU,CAAC,GAAG,CAAC,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IACnD,MAAM,WAAW,GAAG,cAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACrC,MAAM,WAAW,GAAG,cAAI,CAAC,OAAO,CAAC,WAAW,EAAE,UAAU,CAAC,CAAA,CAAC,6BAA6B;IAEvF,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAA;IAErD,MAAM,QAAQ,GAAG,WAAW,GAAG,QAAQ,CAAA;IACvC,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAA;IACjE,MAAM,aAAa,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAA;IAEtE,IAAI,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC5E,MAAM,WAAW,GAAG,OAAO,EAAE,OAAO,EAAE,WAAW,CAAA;IACjD,IAAI,WAAW,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QACpD,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAC7B,CAAC;IAED,IAAI,GAAG,GAAG,CAAC,QAAQ,EAAE,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,QAAQ,IAAI,GAAG,OAAO,CAAC,CAAC,CAAA;IACxF,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,GAAG,GAAG,aAAa,IAAI,GAAG,OAAO,CAAA;IAC9C,CAAC;IAED,OAAO,IAAI,EAAE,CAAA;AACf,CAAC,CAAC,CAAA","sourcesContent":["import Router from 'koa-router'\nimport path from 'path'\n\nimport { searchHelp } from '../help-store.js'\n\nexport const helpRouter = new Router()\n\nvar fs = require('fs')\n\nprocess.on('client-rebuilt' as any, (app, compilerFileSystem) => {\n /* change filesystem whenever webpacking assets done. only for development mode. */\n fs = compilerFileSystem\n})\n\n/*\n * 도움말 검색(ADR-0181) — 화면의 `?` 가 「이 화면의 도움말」을, AI 비서가 질문의 도움말을 같은 색인에서 찾는다.\n * `q` 는 검색어, `scope` 는 주제 경로(`office`), `audience` 는 읽는 사람(`finance`), 언어는 요청의 것.\n */\nhelpRouter.get('/search', async context => {\n const { q = '', scope, audience, limit, coverage } = context.query as Record<string, string | undefined>\n const minCoverage = Number(coverage)\n context.body = searchHelp(String(q), {\n lang: (context as any).language,\n scope: scope || undefined,\n audience: audience || undefined,\n limit: Math.min(Math.max(Number(limit) || 5, 1), 20),\n minCoverage: minCoverage > 0 && minCoverage <= 1 ? minCoverage : undefined\n })\n context.type = 'application/json; charset=UTF-8'\n})\n\nhelpRouter.get('/(.+)/index.md', async (context, next) => {\n const appRootPath = path.resolve('.')\n const PUBLIC_ROOT = path.resolve(appRootPath, 'dist-app') // webpack config output.path\n const { path: pathname } = context\n const folder = pathname.slice(0, pathname.lastIndexOf('/index.md'))\n const folderName = PUBLIC_ROOT + folder\n\n if (fs.existsSync(`${folderName}/index.md`)) {\n context.url = `${pathname}`\n return next()\n }\n\n // If index.md doesn't exist, list topics in the folder\n const topicList = []\n\n // List all files in the folder\n const folderContents = fs.readdirSync(folderName)\n\n // Loop through the files to find matching topics\n for (const file of folderContents) {\n // Check if the file matches the {topic}.{locale}.md pattern\n const topicLocaleMatch = file.match(/(.+)\\.(.+)\\.md/)\n if (topicLocaleMatch) {\n const [, topic, locale] = topicLocaleMatch\n topicList.push(`- [${topic}](${folder}/${topic}.md)`)\n } else {\n // Check if the file matches the {topic}.md pattern\n const topicMatch = file.match(/(.+)\\.md/)\n if (topicMatch) {\n const [, topic] = topicMatch\n topicList.push(`- [${topic}](${folder}/${topic}.md)`)\n }\n }\n }\n\n // If topics are found, return the list in markdown format\n if (topicList.length > 0) {\n const uniqueList = [...new Set(topicList)]\n const topicListMarkdown = [`# ${folder}`, ...uniqueList].join('\\n\\n')\n context.body = topicListMarkdown\n context.type = 'text/markdown; charset=UTF-8'\n } else {\n // If no topics found, you can return a message or handle it as you like\n context.body = 'No topics found in this folder.'\n }\n})\n\nhelpRouter.get('/(.+).md', async (context, next) => {\n const appRootPath = path.resolve('.')\n const PUBLIC_ROOT = path.resolve(appRootPath, 'dist-app') // webpack config output.path\n\n const { language, i18next, path: pathname } = context\n\n const filePath = PUBLIC_ROOT + pathname\n const fileName = filePath.slice(0, filePath.lastIndexOf('.md'))\n const pathnameNoExt = pathname.slice(0, pathname.lastIndexOf('.md'))\n\n var fallbacks = language.indexOf('-') !== -1 ? [language.split('-')[0]] : []\n const fallbackLng = i18next?.options?.fallbackLng\n if (fallbackLng && !fallbacks.includes(fallbackLng)) {\n fallbacks.push(fallbackLng)\n }\n\n var loc = [language, ...fallbacks].find(loc => fs.existsSync(`${fileName}.${loc}.md`))\n if (loc !== undefined) {\n context.url = `${pathnameNoExt}.${loc}.md`\n }\n\n return next()\n})\n\n/*\n * 리치 문서(.html) — 마크다운과 동일한 로케일 해석. ox-doc(ox-markdown 의 리치 짝)이 소비.\n * 정적 파일·로케일 자동 해석 구조를 그대로 확장(호출부는 topic 하나만, 다국어 링크 복잡함 제거).\n */\nhelpRouter.get('/(.+).html', async (context, next) => {\n const appRootPath = path.resolve('.')\n const PUBLIC_ROOT = path.resolve(appRootPath, 'dist-app') // webpack config output.path\n\n const { language, i18next, path: pathname } = context\n\n const filePath = PUBLIC_ROOT + pathname\n const fileName = filePath.slice(0, filePath.lastIndexOf('.html'))\n const pathnameNoExt = pathname.slice(0, pathname.lastIndexOf('.html'))\n\n var fallbacks = language.indexOf('-') !== -1 ? [language.split('-')[0]] : []\n const fallbackLng = i18next?.options?.fallbackLng\n if (fallbackLng && !fallbacks.includes(fallbackLng)) {\n fallbacks.push(fallbackLng)\n }\n\n var loc = [language, ...fallbacks].find(loc => fs.existsSync(`${fileName}.${loc}.html`))\n if (loc !== undefined) {\n context.url = `${pathnameNoExt}.${loc}.html`\n }\n\n return next()\n})\n"]}
|