@get-technology-inc/jamf-docs-mcp-server 5.6.0 → 5.8.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.
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Reading an Intercom Help Center.
3
+ *
4
+ * support.jamf.com is a Next.js app whose every page embeds its own data as
5
+ * JSON in `<script id="__NEXT_DATA__">`. That is the source of truth: the
6
+ * rendered DOM is a view of it, and parsing the JSON avoids guessing at
7
+ * class names that change with any theme update.
8
+ *
9
+ * The content model is a block list, not HTML, so it is rendered to Markdown
10
+ * here rather than going through `content-parser`.
11
+ */
12
+ import * as cheerio from 'cheerio';
13
+ import { httpGetText } from '../http-client.js';
14
+ import { cacheKey } from './cache-key.js';
15
+ import { PAGINATION_CONFIG, TOKEN_CONFIG } from '../constants.js';
16
+ import { calculatePagination, truncateListByTokens, buildPaginationNote, } from './tokenizer.js';
17
+ // ─── __NEXT_DATA__ ──────────────────────────────────────────────
18
+ /**
19
+ * Pull the embedded page data out of an Intercom Help Center page.
20
+ *
21
+ * The opening tag carries a `nonce` attribute, so a regex anchored on
22
+ * `<script id="__NEXT_DATA__" type="application/json">` misses every page.
23
+ * Matching up to the first `>` is what makes it robust to attribute drift.
24
+ */
25
+ export function parseNextData(html) {
26
+ const match = /<script id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/.exec(html);
27
+ if (match?.[1] === undefined) {
28
+ return null;
29
+ }
30
+ try {
31
+ return JSON.parse(match[1]);
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /** `props.pageProps`, or null when the page is not shaped like one. */
38
+ function pageProps(html) {
39
+ const data = parseNextData(html);
40
+ const props = data?.props?.pageProps;
41
+ return typeof props === 'object' && props !== null
42
+ ? props
43
+ : null;
44
+ }
45
+ /**
46
+ * A JSON value read back as a string, or the fallback.
47
+ *
48
+ * `String(value)` on an `unknown` renders an object as "[object Object]" and
49
+ * puts it straight into a title. These payloads come off the wire, so the
50
+ * narrowing has to happen at the boundary rather than being asserted.
51
+ */
52
+ function asString(value, fallback = '') {
53
+ if (typeof value === 'string') {
54
+ return value;
55
+ }
56
+ if (typeof value === 'number') {
57
+ return String(value);
58
+ }
59
+ return fallback;
60
+ }
61
+ /** Inline HTML inside a block's `text`, flattened to Markdown-safe text. */
62
+ function inlineText(html) {
63
+ const $ = cheerio.load(`<div>${html}</div>`);
64
+ $('a[href]').each((_, el) => {
65
+ const href = $(el).attr('href') ?? '';
66
+ const label = $(el).text();
67
+ if (href !== '' && label !== '') {
68
+ $(el).replaceWith(`[${label}](${href})`);
69
+ }
70
+ });
71
+ $('code').each((_, el) => { $(el).replaceWith(`\`${$(el).text()}\``); });
72
+ $('strong, b').each((_, el) => { $(el).replaceWith(`**${$(el).text()}**`); });
73
+ $('em, i').each((_, el) => { $(el).replaceWith(`*${$(el).text()}*`); });
74
+ return $('div').text().replace(/\s+/g, ' ').trim();
75
+ }
76
+ /**
77
+ * Render a nested list.
78
+ *
79
+ * A list item carries its text as a `content` block array — usually a single
80
+ * `paragraph` — not as `text`. The block's own `text` is a pre-rendered
81
+ * string of the whole list ("1. …\n2. …"), which is why reading `item.text`
82
+ * produces empty bullets rather than an obvious error: every item has the
83
+ * field, and it is undefined on all of them.
84
+ *
85
+ * Nested lists arrive as further list blocks inside that same `content`, so
86
+ * they are rendered by recursing through {@link renderBlocks} with the depth
87
+ * carried in the indent.
88
+ */
89
+ function renderList(items, ordered, depth) {
90
+ const indent = ' '.repeat(depth);
91
+ return items
92
+ .map((item, index) => {
93
+ const bullet = ordered ? `${String(index + 1)}.` : '-';
94
+ const body = item.content !== undefined && item.content.length > 0
95
+ ? renderBlocks(item.content, depth + 1).trim()
96
+ : inlineText(item.text ?? '');
97
+ if (body === '') {
98
+ return '';
99
+ }
100
+ const [first = '', ...rest] = body.split('\n');
101
+ const continuation = rest.map(line => `${indent} ${line}`).join('\n');
102
+ return `${indent}${bullet} ${first}\n${continuation === '' ? '' : `${continuation}\n`}`;
103
+ })
104
+ .join('');
105
+ }
106
+ /**
107
+ * The blocks that map to one Markdown construct each.
108
+ *
109
+ * Split from {@link renderBlock} so the two nesting types — `callout` and
110
+ * `collapsibleSection`, which recurse — stay legible next to each other
111
+ * rather than at the bottom of one long switch. Returns null for anything it
112
+ * does not handle.
113
+ */
114
+ function renderSimpleBlock(block, depth) {
115
+ switch (block.type) {
116
+ case 'heading':
117
+ return `## ${inlineText(block.text ?? '')}\n\n`;
118
+ case 'subheading':
119
+ return `### ${inlineText(block.text ?? '')}\n\n`;
120
+ case 'paragraph': {
121
+ const text = inlineText(block.text ?? '');
122
+ return text === '' ? '' : `${text}\n\n`;
123
+ }
124
+ case 'orderedNestedList':
125
+ return `${renderList(block.items ?? [], true, depth)}\n`;
126
+ case 'unorderedNestedList':
127
+ return `${renderList(block.items ?? [], false, depth)}\n`;
128
+ case 'code':
129
+ return `\`\`\`\n${block.text ?? ''}\n\`\`\`\n\n`;
130
+ case 'horizontalRule':
131
+ return '---\n\n';
132
+ case 'image':
133
+ return block.url !== undefined ? `![](${block.url})\n\n` : '';
134
+ case undefined:
135
+ default:
136
+ return null;
137
+ }
138
+ }
139
+ /**
140
+ * Render one Intercom block.
141
+ *
142
+ * Ten types occur across the live corpus, not the four the integration notes
143
+ * listed. `callout` and `collapsibleSection` nest their body under `content`
144
+ * rather than `text`, and `subheading`, `unorderedNestedList`,
145
+ * `collapsibleSection`, `image`, `code` and `horizontalRule` would all be
146
+ * dropped by a renderer that only knew the four — silently, since a missing
147
+ * block leaves no trace in the output.
148
+ */
149
+ export function renderBlock(block, depth = 0) {
150
+ const simple = renderSimpleBlock(block, depth);
151
+ if (simple !== null) {
152
+ return simple;
153
+ }
154
+ switch (block.type) {
155
+ case 'callout':
156
+ // Rendered as a blockquote: a callout is emphasis, and losing it would
157
+ // turn "do not do this" into an ordinary sentence.
158
+ return `${renderBlocks(block.content ?? [])
159
+ .trimEnd()
160
+ .split('\n')
161
+ .map(line => `> ${line}`)
162
+ .join('\n')}\n\n`;
163
+ case 'collapsibleSection':
164
+ return `**${inlineText(block.summary ?? '')}**\n\n${renderBlocks(block.content ?? [])}`;
165
+ case undefined:
166
+ default:
167
+ // Unknown type: keep whatever text it has rather than dropping the
168
+ // block. Intercom adds types over time and silence is the worse
169
+ // failure — a reader cannot tell a missing paragraph from one that was
170
+ // never written.
171
+ return block.text !== undefined ? `${inlineText(block.text)}\n\n` : '';
172
+ }
173
+ }
174
+ export function renderBlocks(blocks, depth = 0) {
175
+ return blocks.map(block => renderBlock(block, depth)).join('');
176
+ }
177
+ /**
178
+ * Parse one Help Center article page.
179
+ *
180
+ * `articleContent.markdown` exists on every article and is `null` on every
181
+ * one measured — the body is in `blocks`. Reading the field that is named
182
+ * for what you want is the trap here.
183
+ */
184
+ export function parseIntercomArticle(html) {
185
+ const props = pageProps(html);
186
+ const article = props?.articleContent;
187
+ if (article === undefined) {
188
+ return null;
189
+ }
190
+ const blocks = (article.blocks ?? []);
191
+ // `breadcrumbs` is a sibling of `articleContent` under pageProps, not a key
192
+ // of it.
193
+ const crumbs = (props?.breadcrumbs ?? []);
194
+ return {
195
+ title: asString(article.title, 'Untitled'),
196
+ content: renderBlocks(blocks).trim(),
197
+ ...(typeof article.description === 'string' && article.description !== ''
198
+ ? { description: article.description } : {}),
199
+ ...(typeof article.lastUpdatedDate === 'string'
200
+ ? { lastUpdated: article.lastUpdatedDate.slice(0, 10) } : {}),
201
+ breadcrumb: crumbs
202
+ .map(crumb => crumb.label ?? crumb.name ?? '')
203
+ .filter(label => label !== ''),
204
+ };
205
+ }
206
+ function slugFromUrl(url) {
207
+ const last = url.replace(/\/$/, '').split('/').pop() ?? '';
208
+ // `/collections/12369024-jamf-pro` → `jamf-pro`. The numeric id is
209
+ // Intercom's and changes if a collection is recreated; the slug is what a
210
+ // reader recognises, so it is what publication ids are built from.
211
+ return last.replace(/^\d+-/, '');
212
+ }
213
+ /** The Help Center's top-level collections for one locale. */
214
+ export async function listIntercomCollections(ctx, source, locale) {
215
+ const key = cacheKey('intercom-collections', { source: source.id, locale });
216
+ const cached = await ctx.cache.get(key);
217
+ if (cached !== null) {
218
+ return cached;
219
+ }
220
+ const html = await httpGetText(`${source.baseUrl}/${locale}/`);
221
+ const props = pageProps(html);
222
+ const home = props?.home;
223
+ const collections = (home?.collections ?? []).map((collection) => {
224
+ const url = asString(collection.url);
225
+ return {
226
+ id: asString(collection.id),
227
+ slug: typeof collection.slug === 'string' && collection.slug !== ''
228
+ ? collection.slug
229
+ : slugFromUrl(url),
230
+ name: asString(collection.name),
231
+ description: asString(collection.description),
232
+ url,
233
+ articleCount: typeof collection.articleCount === 'number' ? collection.articleCount : 0,
234
+ };
235
+ });
236
+ await ctx.cache.set(key, collections, ctx.config.cacheTtl.products);
237
+ return collections;
238
+ }
239
+ /**
240
+ * The article tree of one collection.
241
+ *
242
+ * A collection page's `__NEXT_DATA__` carries the whole subtree —
243
+ * subcollections and every article summary — so one request answers what
244
+ * crawling 356 article pages would.
245
+ */
246
+ export async function fetchIntercomCollectionToc(ctx, source, collection) {
247
+ const key = cacheKey('intercom-collection-toc', { source: source.id, collection: collection.id });
248
+ const cached = await ctx.cache.get(key);
249
+ if (cached !== null) {
250
+ return cached;
251
+ }
252
+ const html = await httpGetText(collection.url);
253
+ const props = pageProps(html);
254
+ const raw = props?.collection;
255
+ const toEntries = (summaries) => (summaries ?? []).map(summary => ({
256
+ title: asString(summary.title, 'Untitled'),
257
+ url: asString(summary.url),
258
+ }));
259
+ const entries = [
260
+ // Articles that sit directly in the collection come first: they are the
261
+ // ones with no subcollection to file them under, and dropping them is
262
+ // the easy mistake — Jamf Pro has 11 of them beside 24 subcollections.
263
+ ...toEntries(raw?.articleSummaries),
264
+ ...(raw?.subcollections ?? []).map((sub) => {
265
+ const children = toEntries(sub.articleSummaries);
266
+ const entry = {
267
+ title: asString(sub.name, 'Untitled'),
268
+ url: asString(sub.url),
269
+ };
270
+ if (children.length > 0) {
271
+ entry.children = children;
272
+ }
273
+ return entry;
274
+ }),
275
+ ];
276
+ await ctx.cache.set(key, entries, ctx.config.cacheTtl.products);
277
+ return entries;
278
+ }
279
+ /**
280
+ * A `FetchTocResult` for one Intercom collection.
281
+ *
282
+ * Pagination and truncation match the other TOC paths, so a caller cannot
283
+ * tell from the response shape which kind of source answered.
284
+ */
285
+ export async function fetchIntercomToc(ctx, source, collection, options = {}) {
286
+ const page = options.page ?? PAGINATION_CONFIG.DEFAULT_PAGE;
287
+ const maxTokens = options.maxTokens ?? TOKEN_CONFIG.DEFAULT_MAX_TOKENS;
288
+ const allToc = await fetchIntercomCollectionToc(ctx, source, collection);
289
+ const count = (entries) => entries.reduce((total, entry) => total + 1 + (entry.children !== undefined ? count(entry.children) : 0), 0);
290
+ const serialise = (entry, depth = 0) => {
291
+ const indent = ' '.repeat(depth);
292
+ const children = entry.children?.map(child => serialise(child, depth + 1)).join('') ?? '';
293
+ return `${indent}- ${entry.title}\n${children}`;
294
+ };
295
+ const calc = calculatePagination(allToc.length, page, PAGINATION_CONFIG.DEFAULT_PAGE_SIZE);
296
+ const { items, tokenCount, truncated } = truncateListByTokens(allToc.slice(calc.startIndex, calc.endIndex), maxTokens, serialise);
297
+ const paginationNote = buildPaginationNote(calc);
298
+ return {
299
+ toc: items,
300
+ pagination: {
301
+ page: calc.page,
302
+ pageSize: calc.pageSize,
303
+ totalPages: calc.totalPages,
304
+ totalItems: count(allToc),
305
+ hasNext: calc.hasNext,
306
+ hasPrev: calc.hasPrev,
307
+ },
308
+ tokenInfo: { tokenCount, truncated, maxTokens },
309
+ ...(paginationNote !== undefined ? { paginationNote } : {}),
310
+ };
311
+ }
312
+ //# sourceMappingURL=intercom-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intercom-service.js","sourceRoot":"","sources":["../../../src/core/services/intercom-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Table of contents for a static documentation source, built from its sitemap.
3
+ *
4
+ * A Fluid Topics publication ships a real TOC endpoint. A static site does
5
+ * not — but concepts.jamf.com publishes a sitemap whose paths already encode
6
+ * the hierarchy (`{locale}/guides/{category}/{article}`), so the tree can be
7
+ * derived from 990 URLs in one request instead of crawling 99 pages per
8
+ * locale and parsing each one's navigation.
9
+ */
10
+ import type { StaticDocSource, StaticSection } from '../constants/sources.js';
11
+ import type { ServerContext } from '../types/context.js';
12
+ import type { FetchTocOptions, FetchTocResult, TocEntry } from '../types.js';
13
+ /** One `<url>` of a sitemap, reduced to what a TOC needs. */
14
+ export interface SitemapEntry {
15
+ /** Canonical absolute URL. */
16
+ url: string;
17
+ /** Path segments after the origin, e.g. `['en', 'guides', 'ai-governance']`. */
18
+ segments: string[];
19
+ /** `<lastmod>`, when present. */
20
+ lastModified?: string;
21
+ }
22
+ /**
23
+ * Extract every `<loc>` from a sitemap, with its `<lastmod>` when it has one.
24
+ *
25
+ * Deliberately a scan for `<url>` blocks rather than an XML parse: the
26
+ * document is a flat list, cheerio would have to be told to treat it as XML,
27
+ * and a sitemap that fails to parse should still yield the entries it does
28
+ * have.
29
+ */
30
+ export declare function parseSitemap(xml: string): SitemapEntry[];
31
+ /** Fetch and cache a source's sitemap. */
32
+ export declare function loadSitemap(ctx: ServerContext, source: StaticDocSource): Promise<SitemapEntry[]>;
33
+ /**
34
+ * Turn a slug into a heading: `ai-governance` → `AI Governance`.
35
+ *
36
+ * Checked against the fourteen real titles concepts.jamf.com's own guides
37
+ * index renders, which is the only place the site publishes them without a
38
+ * per-page request: eight of nine match exactly. The ninth is
39
+ * `infrastructure-as-code`, which the site titles "Infrastructure As Code"
40
+ * and this produces as "Infrastructure as Code" — standard title case
41
+ * lowercases "as", and matching one page's capitalisation is not worth a
42
+ * special case.
43
+ */
44
+ export declare function titleFromSlug(slug: string): string;
45
+ /**
46
+ * Build a TOC for one section of a static source, in one locale.
47
+ *
48
+ * @param locale the source's own locale code, e.g. `en` — not `en-US`
49
+ */
50
+ export declare function buildStaticToc(ctx: ServerContext, source: StaticDocSource, section: StaticSection, locale: string): Promise<TocEntry[]>;
51
+ /**
52
+ * A `FetchTocResult` for a static source's section.
53
+ *
54
+ * Pagination and token truncation are the same operations the Fluid Topics
55
+ * path performs, applied to entries that came from a sitemap instead of a
56
+ * map: a caller paging through a Concepts TOC must not get a different shape
57
+ * from one paging through Jamf Pro's.
58
+ */
59
+ export declare function fetchStaticToc(ctx: ServerContext, source: StaticDocSource, section: StaticSection, sourceLocale: string, options?: FetchTocOptions): Promise<FetchTocResult>;
60
+ //# sourceMappingURL=sitemap-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap-service.d.ts","sourceRoot":"","sources":["../../../src/core/services/sitemap-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC9E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAQ7E,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,8BAA8B;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,gFAAgF;IAChF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,iCAAiC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,EAAE,CAmBxD;AAED,0CAA0C;AAC1C,wBAAsB,WAAW,CAC/B,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,YAAY,EAAE,CAAC,CASzB;AA4BD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAUlD;AAsBD;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,QAAQ,EAAE,CAAC,CAwBrB;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,aAAa,EACtB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAgCzB"}
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Table of contents for a static documentation source, built from its sitemap.
3
+ *
4
+ * A Fluid Topics publication ships a real TOC endpoint. A static site does
5
+ * not — but concepts.jamf.com publishes a sitemap whose paths already encode
6
+ * the hierarchy (`{locale}/guides/{category}/{article}`), so the tree can be
7
+ * derived from 990 URLs in one request instead of crawling 99 pages per
8
+ * locale and parsing each one's navigation.
9
+ */
10
+ import { httpGetText } from '../http-client.js';
11
+ import { cacheKey } from './cache-key.js';
12
+ import { canonicalStaticUrl } from './static-article-service.js';
13
+ import { PAGINATION_CONFIG, TOKEN_CONFIG } from '../constants.js';
14
+ import { calculatePagination, truncateListByTokens, buildPaginationNote, } from './tokenizer.js';
15
+ /**
16
+ * Extract every `<loc>` from a sitemap, with its `<lastmod>` when it has one.
17
+ *
18
+ * Deliberately a scan for `<url>` blocks rather than an XML parse: the
19
+ * document is a flat list, cheerio would have to be told to treat it as XML,
20
+ * and a sitemap that fails to parse should still yield the entries it does
21
+ * have.
22
+ */
23
+ export function parseSitemap(xml) {
24
+ const out = [];
25
+ for (const block of xml.match(/<url\b[\s\S]*?<\/url>/g) ?? []) {
26
+ const loc = /<loc>\s*([^<\s]+)\s*<\/loc>/.exec(block)?.[1];
27
+ if (loc === undefined) {
28
+ continue;
29
+ }
30
+ const lastmod = /<lastmod>\s*([^<\s]+)\s*<\/lastmod>/.exec(block)?.[1];
31
+ let segments;
32
+ try {
33
+ segments = new URL(loc).pathname.split('/').filter(Boolean);
34
+ }
35
+ catch {
36
+ continue;
37
+ }
38
+ out.push({
39
+ url: canonicalStaticUrl(loc),
40
+ segments,
41
+ ...(lastmod !== undefined ? { lastModified: lastmod } : {}),
42
+ });
43
+ }
44
+ return out;
45
+ }
46
+ /** Fetch and cache a source's sitemap. */
47
+ export async function loadSitemap(ctx, source) {
48
+ const key = cacheKey('static-sitemap', { source: source.id });
49
+ const cached = await ctx.cache.get(key);
50
+ if (cached !== null) {
51
+ return cached;
52
+ }
53
+ const xml = await httpGetText(`${source.baseUrl}/sitemap.xml`);
54
+ const entries = parseSitemap(xml);
55
+ await ctx.cache.set(key, entries, ctx.config.cacheTtl.products);
56
+ return entries;
57
+ }
58
+ /**
59
+ * Terms whose casing a naive capitalise gets wrong.
60
+ *
61
+ * The sitemap gives slugs, not titles, and fetching 99 pages per locale to
62
+ * read each `og:title` is not worth one heading apiece. Word-by-word
63
+ * capitalisation produces "Ai Governance", "Byod" and "Ios", which read as
64
+ * mistakes; this table is what the site's own index page shows those as.
65
+ * Keyed lowercase.
66
+ */
67
+ const TITLE_CASE_TERMS = {
68
+ ai: 'AI', api: 'API', byod: 'BYOD', ddm: 'DDM', it: 'IT', mdm: 'MDM',
69
+ pki: 'PKI', ldap: 'LDAP', scep: 'SCEP', ztna: 'ZTNA', sso: 'SSO',
70
+ vpn: 'VPN', mfa: 'MFA', dns: 'DNS', ip: 'IP', tls: 'TLS', url: 'URL',
71
+ json: 'JSON', xml: 'XML', sdk: 'SDK', cli: 'CLI', ui: 'UI', ux: 'UX',
72
+ id: 'ID', edr: 'EDR', xdr: 'XDR', siem: 'SIEM', saas: 'SaaS',
73
+ macos: 'macOS', ios: 'iOS', ipados: 'iPadOS', tvos: 'tvOS',
74
+ watchos: 'watchOS', visionos: 'visionOS', jamf: 'Jamf', apple: 'Apple',
75
+ aws: 'AWS', okta: 'Okta', entra: 'Entra', jss: 'JSS',
76
+ };
77
+ /** Words that stay lowercase unless they open the title. */
78
+ const TITLE_MINOR_WORDS = new Set([
79
+ 'a', 'an', 'and', 'as', 'at', 'but', 'by', 'for', 'in', 'of', 'on', 'or',
80
+ 'the', 'to', 'via', 'with',
81
+ ]);
82
+ /**
83
+ * Turn a slug into a heading: `ai-governance` → `AI Governance`.
84
+ *
85
+ * Checked against the fourteen real titles concepts.jamf.com's own guides
86
+ * index renders, which is the only place the site publishes them without a
87
+ * per-page request: eight of nine match exactly. The ninth is
88
+ * `infrastructure-as-code`, which the site titles "Infrastructure As Code"
89
+ * and this produces as "Infrastructure as Code" — standard title case
90
+ * lowercases "as", and matching one page's capitalisation is not worth a
91
+ * special case.
92
+ */
93
+ export function titleFromSlug(slug) {
94
+ const words = slug.split('-').filter(Boolean);
95
+ return words
96
+ .map((word, index) => {
97
+ const known = TITLE_CASE_TERMS[word.toLowerCase()];
98
+ if (known !== undefined) {
99
+ return known;
100
+ }
101
+ if (index > 0 && TITLE_MINOR_WORDS.has(word.toLowerCase())) {
102
+ return word.toLowerCase();
103
+ }
104
+ return word.charAt(0).toUpperCase() + word.slice(1);
105
+ })
106
+ .join(' ');
107
+ }
108
+ function toTocEntries(nodes, titles) {
109
+ return [...nodes]
110
+ .sort((a, b) => a.slug.localeCompare(b.slug))
111
+ .map(node => {
112
+ const children = toTocEntries(node.children.values(), titles);
113
+ const entry = {
114
+ title: titles.get(node.url ?? '') ?? titleFromSlug(node.slug),
115
+ url: node.url ?? '',
116
+ };
117
+ if (children.length > 0) {
118
+ entry.children = children;
119
+ }
120
+ return entry;
121
+ });
122
+ }
123
+ /**
124
+ * Build a TOC for one section of a static source, in one locale.
125
+ *
126
+ * @param locale the source's own locale code, e.g. `en` — not `en-US`
127
+ */
128
+ export async function buildStaticToc(ctx, source, section, locale) {
129
+ const entries = await loadSitemap(ctx, source);
130
+ const root = new Map();
131
+ for (const entry of entries) {
132
+ const [entryLocale, entrySection, ...rest] = entry.segments;
133
+ if (entryLocale !== locale || entrySection !== section.path) {
134
+ continue;
135
+ }
136
+ // The section's own index page is the container, not a child of itself.
137
+ if (rest.length === 0) {
138
+ continue;
139
+ }
140
+ let level = root;
141
+ let node;
142
+ for (const slug of rest) {
143
+ node = level.get(slug);
144
+ if (node === undefined) {
145
+ node = { slug, children: new Map() };
146
+ level.set(slug, node);
147
+ }
148
+ level = node.children;
149
+ }
150
+ if (node !== undefined) {
151
+ node.url = entry.url;
152
+ }
153
+ }
154
+ return toTocEntries(root.values(), new Map());
155
+ }
156
+ /**
157
+ * A `FetchTocResult` for a static source's section.
158
+ *
159
+ * Pagination and token truncation are the same operations the Fluid Topics
160
+ * path performs, applied to entries that came from a sitemap instead of a
161
+ * map: a caller paging through a Concepts TOC must not get a different shape
162
+ * from one paging through Jamf Pro's.
163
+ */
164
+ export async function fetchStaticToc(ctx, source, section, sourceLocale, options = {}) {
165
+ const page = options.page ?? PAGINATION_CONFIG.DEFAULT_PAGE;
166
+ const maxTokens = options.maxTokens ?? TOKEN_CONFIG.DEFAULT_MAX_TOKENS;
167
+ const allToc = await buildStaticToc(ctx, source, section, sourceLocale);
168
+ const totalItems = countTocEntries(allToc);
169
+ const paginationCalc = calculatePagination(allToc.length, page, PAGINATION_CONFIG.DEFAULT_PAGE_SIZE);
170
+ const paginated = allToc.slice(paginationCalc.startIndex, paginationCalc.endIndex);
171
+ const { items, tokenCount, truncated } = truncateListByTokens(paginated, maxTokens, tocEntryToString);
172
+ const paginationNote = buildPaginationNote(paginationCalc);
173
+ return {
174
+ toc: items,
175
+ pagination: {
176
+ page: paginationCalc.page,
177
+ pageSize: paginationCalc.pageSize,
178
+ totalPages: paginationCalc.totalPages,
179
+ totalItems,
180
+ hasNext: paginationCalc.hasNext,
181
+ hasPrev: paginationCalc.hasPrev,
182
+ },
183
+ tokenInfo: { tokenCount, truncated, maxTokens },
184
+ // The locale that answered is the one asked for: unlike Fluid Topics,
185
+ // where a family may exist in en-US only, a static section either
186
+ // publishes the locale or `resolveTocSource` refused before reaching here.
187
+ resolvedLocale: sourceLocale,
188
+ ...(paginationNote !== undefined ? { paginationNote } : {}),
189
+ };
190
+ }
191
+ /** Total entries including nested children. Mirrors toc-service's own count. */
192
+ function countTocEntries(entries) {
193
+ return entries.reduce((count, entry) => count + 1 + (entry.children !== undefined ? countTocEntries(entry.children) : 0), 0);
194
+ }
195
+ /** Serialise one entry for token estimation. Mirrors toc-service's. */
196
+ function tocEntryToString(entry, depth = 0) {
197
+ const indent = ' '.repeat(depth);
198
+ const childrenStr = entry.children?.map(c => tocEntryToString(c, depth + 1)).join('') ?? '';
199
+ return `${indent}- ${entry.title}\n${childrenStr}`;
200
+ }
201
+ //# sourceMappingURL=sitemap-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap-service.js","sourceRoot":"","sources":["../../../src/core/services/sitemap-service.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"static-article-service.d.ts","sourceRoot":"","sources":["../../../src/core/services/static-article-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAQH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAU3E;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAWzD;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASrE;AAYD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CA2D7B"}
1
+ {"version":3,"file":"static-article-service.d.ts","sourceRoot":"","sources":["../../../src/core/services/static-article-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAQH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAa3E;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAWzD;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASrE;AAYD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,aAAa,EAClB,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CA2D7B"}