@kyyinfinite/lumina 1.0.0 → 1.0.1

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/src/index.js CHANGED
@@ -1,151 +1,50 @@
1
- /**
2
- * @file index.js
3
- * @module @kyyinfinite/lumina
4
- *
5
- * Lumina — Modern WhatsApp framework built on top of Baileys.
6
- *
7
- * Public API surface:
8
- *
9
- * import { Bot } from '@kyyinfinite/lumina'
10
- * const bot = new Bot(socket)
11
- * await bot.text(jid, 'Halo')
12
- *
13
- * Subpath exports (tree-shakable):
14
- *
15
- * import { extractInlineEntities } from '@kyyinfinite/lumina/parsers'
16
- * import { MediaService } from '@kyyinfinite/lumina/services'
17
- * import { ButtonBuilder } from '@kyyinfinite/lumina/builders'
18
- * import { ProtoUpdater } from '@kyyinfinite/lumina/proto'
19
- * import { resize } from '@kyyinfinite/lumina/media'
20
- */
21
-
22
1
  import { Bot } from './client/bot.js'
23
2
  import { Connection } from './client/connection.js'
24
-
25
3
  import { ButtonBuilder } from './builders/button.js'
26
4
  import { ButtonV2Builder } from './builders/button-v2.js'
27
5
  import { CarouselBuilder } from './builders/carousel.js'
28
6
  import { CardBuilder } from './builders/card.js'
29
7
  import { AIRichBuilder } from './builders/ai-rich.js'
30
-
31
8
  import { MediaService } from './services/media-service.js'
32
9
  import { ProtoService } from './services/proto-service.js'
33
10
  import { MessageService } from './services/message-service.js'
34
-
35
11
  import { extractInlineEntities } from './parsers/inline-entity.js'
36
12
  import { tokenizeCode } from './parsers/code-tokenizer.js'
37
13
  import { toTableMetadata } from './parsers/table-metadata.js'
38
-
39
14
  import { ProtoUpdater, transformToESM, applyKnownFixes } from './proto/updater.js'
40
15
  import { assembleRichResponse } from './proto/rich-response.js'
41
16
  import { createInteractiveNodes, createBareInteractiveNodes } from './proto/relay-nodes.js'
42
-
43
- // Catalog re-exports — these are imported so that the named exports below
44
- // are available from the root `@kyyinfinite/lumina` entry point.
45
17
  import {
46
- MessageType,
47
- ForwardOrigin,
48
- HeaderType,
49
- NativeFlow,
50
- BOT_JID,
51
- LayoutKind,
52
- HighlightType,
53
- HighlightLabel,
54
- ImagineType,
55
- SourceType,
56
- PromptType,
57
- SessionTransparencyType,
58
- TYPENAME,
18
+ MessageType, ForwardOrigin, HeaderType, NativeFlow, BOT_JID, LayoutKind,
19
+ HighlightType, HighlightLabel, ImagineType, SourceType, PromptType,
20
+ SessionTransparencyType, TYPENAME, SimpleButtonType,
59
21
  } from './proto/enums.js'
60
-
61
22
  import {
62
- markdownTextPrimitive,
63
- codePrimitive,
64
- tablePrimitive,
65
- searchResultPrimitive,
66
- reelPrimitive,
67
- imaginePrimitive,
68
- productCardPrimitive,
69
- postPrimitive,
70
- metadataTextPrimitive,
71
- followUpSuggestionPillPrimitive,
72
- shapeSourceEntry,
73
- shapeReelEntry,
23
+ markdownTextPrimitive, codePrimitive, tablePrimitive, searchResultPrimitive,
24
+ reelPrimitive, imaginePrimitive, productCardPrimitive, postPrimitive,
25
+ metadataTextPrimitive, followUpSuggestionPillPrimitive, shapeSourceEntry, shapeReelEntry,
74
26
  } from './proto/primitives.js'
75
-
76
27
  import { singleLayout, hscrollLayout, actionRowLayout, layoutFor } from './proto/layouts.js'
77
-
78
28
  import { LuminaError, ValidationError, MediaError, ProtoError, ConnectionError, ProtocolError } from './errors.js'
79
29
  import { createLogger } from './utils/logger.js'
80
30
 
81
- /** Semver version of the Lumina package. */
82
31
  export const VERSION = '1.0.0'
83
32
 
84
- // Public API
85
33
  export {
86
- Bot,
87
- Connection,
88
- // Builders
89
- ButtonBuilder,
90
- ButtonV2Builder,
91
- CarouselBuilder,
92
- CardBuilder,
93
- AIRichBuilder,
94
- // Services
95
- MediaService,
96
- ProtoService,
97
- MessageService,
98
- // Parsers
99
- extractInlineEntities,
100
- tokenizeCode,
101
- toTableMetadata,
102
- // Proto
103
- ProtoUpdater,
104
- transformToESM,
105
- applyKnownFixes,
106
- assembleRichResponse,
107
- createInteractiveNodes,
108
- createBareInteractiveNodes,
109
- // Catalog
110
- MessageType,
111
- ForwardOrigin,
112
- HeaderType,
113
- NativeFlow,
114
- BOT_JID,
115
- LayoutKind,
116
- HighlightType,
117
- HighlightLabel,
118
- ImagineType,
119
- SourceType,
120
- PromptType,
121
- SessionTransparencyType,
122
- TYPENAME,
123
- // Primitive factories
124
- markdownTextPrimitive,
125
- codePrimitive,
126
- tablePrimitive,
127
- searchResultPrimitive,
128
- reelPrimitive,
129
- imaginePrimitive,
130
- productCardPrimitive,
131
- postPrimitive,
132
- metadataTextPrimitive,
133
- followUpSuggestionPillPrimitive,
134
- shapeSourceEntry,
135
- shapeReelEntry,
136
- // Layout factories
137
- singleLayout,
138
- hscrollLayout,
139
- actionRowLayout,
140
- layoutFor,
141
- // Errors
142
- LuminaError,
143
- ValidationError,
144
- MediaError,
145
- ProtoError,
146
- ConnectionError,
147
- ProtocolError,
148
- // Utils
34
+ Bot, Connection,
35
+ ButtonBuilder, ButtonV2Builder, CarouselBuilder, CardBuilder, AIRichBuilder,
36
+ MediaService, ProtoService, MessageService,
37
+ extractInlineEntities, tokenizeCode, toTableMetadata,
38
+ ProtoUpdater, transformToESM, applyKnownFixes,
39
+ assembleRichResponse, createInteractiveNodes, createBareInteractiveNodes,
40
+ MessageType, ForwardOrigin, HeaderType, NativeFlow, BOT_JID, LayoutKind,
41
+ HighlightType, HighlightLabel, ImagineType, SourceType, PromptType,
42
+ SessionTransparencyType, TYPENAME, SimpleButtonType,
43
+ markdownTextPrimitive, codePrimitive, tablePrimitive, searchResultPrimitive,
44
+ reelPrimitive, imaginePrimitive, productCardPrimitive, postPrimitive,
45
+ metadataTextPrimitive, followUpSuggestionPillPrimitive, shapeSourceEntry, shapeReelEntry,
46
+ singleLayout, hscrollLayout, actionRowLayout, layoutFor,
47
+ LuminaError, ValidationError, MediaError, ProtoError, ConnectionError, ProtocolError,
149
48
  createLogger,
150
49
  }
151
50
 
@@ -1,29 +1,5 @@
1
- /**
2
- * @file fetch.js
3
- * @module lumina/media/fetch
4
- *
5
- * HTTP fetcher for media URLs. Returns a Buffer. Honours a configurable
6
- * timeout and an opt-in `silent` mode (default OFF — fail loud).
7
- */
8
-
9
1
  import { MediaError } from '../errors.js'
10
2
 
11
- /**
12
- * @typedef {object} FetchOptions
13
- * @property {boolean} [silent=false] Swallow HTTP errors and return Buffer.alloc(0).
14
- * @property {number} [timeout=30000] Request timeout in milliseconds.
15
- * @property {Record<string, string>} [headers] Additional request headers.
16
- * @property {AbortSignal} [signal] External abort signal.
17
- */
18
-
19
- /**
20
- * Fetch a URL and return its body as a Buffer.
21
- *
22
- * @param {string} url
23
- * @param {FetchOptions} [opts]
24
- * @returns {Promise<Buffer>}
25
- * @throws {MediaError} When `silent` is false (default) and the request fails.
26
- */
27
3
  export async function fetchBuffer(url, opts = {}) {
28
4
  const { silent = false, timeout = 30_000, headers, signal } = opts
29
5
 
@@ -34,23 +10,16 @@ export async function fetchBuffer(url, opts = {}) {
34
10
 
35
11
  const controller = new AbortController()
36
12
  const timer = setTimeout(() => controller.abort(), timeout)
37
- // Combine external signal with our timeout signal.
38
13
  if (signal) {
39
14
  if (signal.aborted) controller.abort()
40
15
  else signal.addEventListener('abort', () => controller.abort(), { once: true })
41
16
  }
42
17
 
43
- const response = await fetch(url, {
44
- headers,
45
- signal: controller.signal,
46
- redirect: 'follow',
47
- })
18
+ const response = await fetch(url, { headers, signal: controller.signal, redirect: 'follow' })
48
19
  clearTimeout(timer)
49
20
 
50
21
  if (!response.ok) {
51
- throw new MediaError(`HTTP ${response.status} for ${url}`, {
52
- code: 'LUMINA_MEDIA_HTTP_ERROR',
53
- })
22
+ throw new MediaError(`HTTP ${response.status} for ${url}`, { code: 'LUMINA_MEDIA_HTTP_ERROR' })
54
23
  }
55
24
 
56
25
  return Buffer.from(await response.arrayBuffer())
@@ -1,21 +1,7 @@
1
- /**
2
- * @file image.js
3
- * @module lumina/media/image
4
- *
5
- * Image resizing via `sharp`. Sharp is an optional peer dependency — the
6
- * module lazy-imports it on first use so that users who never touch image
7
- * resizing do not need it installed.
8
- */
9
-
10
1
  import { MediaError } from '../errors.js'
11
2
 
12
3
  let sharpPromise = null
13
4
 
14
- /**
15
- * Lazy-load sharp. Cached so the import cost is paid only once.
16
- *
17
- * @returns {Promise<typeof import('sharp')>}
18
- */
19
5
  async function loadSharp() {
20
6
  if (!sharpPromise) {
21
7
  sharpPromise = (async () => {
@@ -32,27 +18,9 @@ async function loadSharp() {
32
18
  return sharpPromise
33
19
  }
34
20
 
35
- /**
36
- * @typedef {object} ResizeOptions
37
- * @property {number} [width]
38
- * @property {number} [height]
39
- * @property {'cover'|'contain'|'inside'|'outside'} [fit='cover']
40
- * @property {'png'|'jpeg'|'webp'} [format='png']
41
- * @property {object} [background] Background colour for transparent areas.
42
- */
43
-
44
- /**
45
- * Resize a buffer containing an image. Returns a Buffer in the requested format.
46
- *
47
- * @param {Buffer} buffer
48
- * @param {ResizeOptions} [opts]
49
- * @returns {Promise<Buffer>}
50
- */
51
21
  export async function resize(buffer, opts = {}) {
52
22
  if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
53
- throw new MediaError('resize() requires a non-empty Buffer', {
54
- code: 'LUMINA_MEDIA_EMPTY_BUFFER',
55
- })
23
+ throw new MediaError('resize() requires a non-empty Buffer', { code: 'LUMINA_MEDIA_EMPTY_BUFFER' })
56
24
  }
57
25
 
58
26
  const { width = 300, height = 300, fit = 'cover', format = 'png', background } = opts
@@ -71,14 +39,6 @@ export async function resize(buffer, opts = {}) {
71
39
  return pipeline.toBuffer()
72
40
  }
73
41
 
74
- /**
75
- * Convenience: resize to a square thumbnail (default 300×300 PNG cover).
76
- *
77
- * @param {Buffer} buffer
78
- * @param {number} [size=300]
79
- * @param {ResizeOptions['format']} [format]
80
- * @returns {Promise<Buffer>}
81
- */
82
42
  export async function thumbnail(buffer, size = 300, format = 'png') {
83
43
  return resize(buffer, { width: size, height: size, fit: 'cover', format })
84
44
  }
@@ -1,16 +1,3 @@
1
- /**
2
- * @file resolver.js
3
- * @module lumina/media/resolver
4
- *
5
- * Unified media resolver. Accepts URL | base64 string | Buffer | array-of-any
6
- * and returns a normalised representation per the requested strategy.
7
- *
8
- * Bug fix vs legacy: the original `Toolkit.resolveMedia` had two near-identical
9
- * branches (WAUrl vs non-WAUrl) that both called `fetchBuffer` — collapsed
10
- * into a single, explicit 3-strategy switch (`auto | url-only | buffer |
11
- * base64 | upload`).
12
- */
13
-
14
1
  import { MediaError } from '../errors.js'
15
2
  import { fetchBuffer } from './fetch.js'
16
3
  import { resize } from './image.js'
@@ -19,42 +6,14 @@ import { uploadToWhatsApp } from './uploader.js'
19
6
  const HTTP_RE = /^https?:\/\/.+/i
20
7
  const WA_HOST_RE = /^https?:\/\/[^/]*\.whatsapp\.net\//i
21
8
 
22
- /** @typedef {'auto'|'url-only'|'buffer'|'base64'|'upload'} ResolveStrategy */
23
-
24
- /**
25
- * @typedef {object} ResolveOptions
26
- * @property {'image'|'video'|'audio'|'document'} [mediaType='image']
27
- * @property {ResolveStrategy} [strategy='auto']
28
- * @property {{ width: number, height: number, fit?: string }} [resize] Resize before returning (buffer/base64 only).
29
- * @property {boolean} [silent=false]
30
- * @property {string} [jid] Override upload JID.
31
- */
32
-
33
- /**
34
- * Resolve a single media source.
35
- *
36
- * @param {import('../client/connection.js').Connection} conn
37
- * @param {string | Buffer} source
38
- * @param {ResolveOptions} [opts]
39
- * @returns {Promise<string | Buffer>}
40
- */
41
9
  export async function resolveMedia(conn, source, opts = {}) {
42
- const {
43
- mediaType = 'image',
44
- strategy = 'auto',
45
- resize: resizeOpts,
46
- silent = false,
47
- jid,
48
- } = opts
10
+ const { mediaType = 'image', strategy = 'auto', resize: resizeOpts, silent = false, jid } = opts
49
11
 
50
- // Array support: recurse with identical opts.
51
12
  if (Array.isArray(source)) {
52
13
  return Promise.all(source.map((s) => resolveMedia(conn, s, opts)))
53
14
  }
54
15
 
55
16
  const originalIsBuffer = Buffer.isBuffer(source)
56
-
57
- // Stage 1: normalise to either { url } or { buffer }.
58
17
  let buffer = null
59
18
  let url = null
60
19
 
@@ -62,12 +21,9 @@ export async function resolveMedia(conn, source, opts = {}) {
62
21
  if (HTTP_RE.test(source)) {
63
22
  const isWaUrl = WA_HOST_RE.test(source)
64
23
  if (strategy === 'url-only' && isWaUrl) return source
65
- // For HTTP URLs we always need the bytes — either because the user
66
- // asked for buffer/base64, or because we need to upload to WhatsApp.
67
24
  buffer = await fetchBuffer(source, { silent: true })
68
25
  url = source
69
26
  } else {
70
- // Treat as base64.
71
27
  buffer = Buffer.from(source, 'base64')
72
28
  }
73
29
  } else if (originalIsBuffer) {
@@ -85,12 +41,8 @@ export async function resolveMedia(conn, source, opts = {}) {
85
41
  })
86
42
  }
87
43
 
88
- // Stage 2: optional resize.
89
- if (resizeOpts && buffer) {
90
- buffer = await resize(buffer, resizeOpts)
91
- }
44
+ if (resizeOpts && buffer) buffer = await resize(buffer, resizeOpts)
92
45
 
93
- // Stage 3: format per strategy.
94
46
  switch (strategy) {
95
47
  case 'buffer':
96
48
  return buffer
@@ -98,16 +50,12 @@ export async function resolveMedia(conn, source, opts = {}) {
98
50
  return buffer.toString('base64')
99
51
  case 'url-only':
100
52
  if (url && WA_HOST_RE.test(url)) return url
101
- throw new MediaError('url-only strategy requires a wa.me URL', {
102
- code: 'LUMINA_MEDIA_URL_ONLY_FAILED',
103
- })
53
+ throw new MediaError('url-only strategy requires a wa.me URL', { code: 'LUMINA_MEDIA_URL_ONLY_FAILED' })
104
54
  case 'upload':
105
55
  return uploadToWhatsApp(conn, buffer, mediaType, { jid })
106
56
  case 'auto':
107
57
  default:
108
58
  if (url && WA_HOST_RE.test(url)) return url
109
- if (originalIsBuffer) return uploadToWhatsApp(conn, buffer, mediaType, { jid })
110
- // We have a non-WA URL — upload to WhatsApp so the consumer gets a CDN URL.
111
59
  return uploadToWhatsApp(conn, buffer, mediaType, { jid })
112
60
  }
113
61
  }
@@ -1,37 +1,7 @@
1
- /**
2
- * @file uploader.js
3
- * @module lumina/media/uploader
4
- *
5
- * Thin wrapper around Baileys' `prepareWAMessageMedia` that uploads a buffer
6
- * (or `{ url }`) to WhatsApp and returns the resulting CDN URL.
7
- *
8
- * Bug fix vs legacy: the hardcoded `jid: '@newsletter'` (which silently
9
- * forced every media upload through the newsletter queue) is replaced by a
10
- * configurable default that the user can override via the `Connection`
11
- * options or per-call `jid`.
12
- */
13
-
14
1
  import { MediaError } from '../errors.js'
15
2
 
16
- /**
17
- * Default pre-upload JID. Use a stable bot JID that WhatsApp accepts for
18
- * media pre-uploads — historically the legacy code used `@newsletter`, but
19
- * that route is rate-limited and intended for broadcast use. The default
20
- * `s.whatsapp.net` route is more permissive and works for bot accounts.
21
- */
22
3
  export const DEFAULT_UPLOAD_JID = '62831@s.whatsapp.net'
23
4
 
24
- /**
25
- * Upload a media source to WhatsApp and return its CDN URL.
26
- *
27
- * @param {import('../client/connection.js').Connection} conn
28
- * @param {Buffer | { url: string }} source
29
- * @param {'image'|'video'|'audio'|'document'} mediaType
30
- * @param {object} [opts]
31
- * @param {string} [opts.jid=DEFAULT_UPLOAD_JID] Pre-upload JID.
32
- * @param {object} [opts.options] Extra options forwarded to `prepareWAMessageMedia`.
33
- * @returns {Promise<string>} CDN URL.
34
- */
35
5
  export async function uploadToWhatsApp(conn, source, mediaType, opts = {}) {
36
6
  if (!conn?.uploadMedia) {
37
7
  throw new MediaError('connection.uploadMedia is not available', {
@@ -1,17 +1,6 @@
1
- /**
2
- * @file video.js
3
- * @module lumina/media/video
4
1
  *
5
- * Pure-Node MP4 box parser (for duration) + ffmpeg-based thumbnail extractor.
6
2
  *
7
- * The legacy `Toolkit.getMp4Duration` was a hand-rolled ISO-BMFF box walker
8
- * that worked but had no tests and several silent-failure paths. Lumina
9
- * keeps the same approach (no native deps for duration) but factors it into
10
- * a single, documented function.
11
3
  *
12
- * `extractThumbnail` lazy-imports `fluent-ffmpeg` — users who never send
13
- * video thumbnails do not need it installed.
14
- */
15
4
 
16
5
  import { PassThrough, Readable } from 'node:stream'
17
6
 
@@ -36,19 +25,8 @@ async function loadFfmpeg() {
36
25
  return ffmpegPromise
37
26
  }
38
27
 
39
- /**
40
- * Walk the ISO-BMFF box tree of an MP4 buffer and extract the duration
41
- * (in seconds) from the first `mvhd` box found inside `moov`.
42
28
  *
43
- * Returns 0 when the buffer is invalid, truncated, or lacks a `moov`/`mvhd`
44
- * box — `silent: true` (default) preserves the legacy swallow-on-failure
45
- * behaviour so that video uploads do not hard-fail when metadata is missing.
46
29
  *
47
- * @param {Buffer} buffer
48
- * @param {object} [opts]
49
- * @param {boolean} [opts.silent=true]
50
- * @returns {number} Duration in seconds (0 on failure).
51
- */
52
30
  export function getMp4Duration(buffer, opts = {}) {
53
31
  const { silent = true } = opts
54
32
 
@@ -109,24 +87,8 @@ export function getMp4Duration(buffer, opts = {}) {
109
87
  }
110
88
  }
111
89
 
112
- /**
113
- * @typedef {object} ThumbnailOptions
114
- * @property {number|string} [time='auto'] Seek position (seconds) or 'auto' (= 20% of duration, capped at 10s).
115
- * @property {number} [width=300]
116
- * @property {number} [height=300]
117
- * @property {'png'|'jpeg'|'webp'} [format='png']
118
- * @property {boolean} [resizeOutput=true] Pass the frame through `sharp.resize()`.
119
- * @property {boolean} [silent=true] Swallow ffmpeg errors and return empty Buffer.
120
- * @property {'buffer'|'base64'} [result='buffer']
121
- */
122
-
123
- /**
124
- * Extract a single frame from an MP4 buffer at the given timestamp.
90
+
125
91
  *
126
- * @param {Buffer} videoBuffer
127
- * @param {ThumbnailOptions} [opts]
128
- * @returns {Promise<Buffer|string>} Buffer or base64 string (per `result`).
129
- */
130
92
  export async function extractThumbnail(videoBuffer, opts = {}) {
131
93
  const {
132
94
  time,
@@ -1,15 +1,6 @@
1
- /**
2
- * @file code-tokenizer-keywords.js
3
- * @module lumina/parsers/code-tokenizer-keywords
4
1
  *
5
- * Language keyword catalog. Built ONCE at module-load time (vs the legacy
6
- * `AIRich.tokenizer` which rebuilt a `new Set(...)` on every call).
7
2
  *
8
- * Twelve languages are supported out of the box. To add a new language,
9
- * append a `Set` here — the lexer picks it up automatically.
10
- */
11
3
 
12
- /** @type {Record<string, Set<string>>} */
13
4
  export const KEYWORDS = {
14
5
  javascript: new Set([
15
6
  'break', 'case', 'catch', 'continue', 'debugger', 'default', 'delete', 'do', 'else',
@@ -112,15 +103,12 @@ export const KEYWORDS = {
112
103
  markdown: new Set(['#', '##', '###', '####', '#####', '######']),
113
104
  }
114
105
 
115
- /** Languages where `//` introduces a line comment. */
116
106
  export const SLASH_COMMENT_LANGS = new Set([
117
107
  'javascript', 'typescript', 'java', 'golang', 'c', 'cpp', 'php', 'rust', 'css',
118
108
  ])
119
109
 
120
- /** Languages where `#` introduces a line comment. */
121
110
  export const HASH_COMMENT_LANGS = new Set(['python', 'bash', 'php', 'rust'])
122
111
 
123
- /** Languages that support block comments with `/* ... *\/`. */
124
112
  export const BLOCK_COMMENT_LANGS = new Set([
125
113
  'javascript', 'typescript', 'java', 'c', 'cpp', 'php', 'css',
126
114
  ])
@@ -1,31 +1,11 @@
1
- /**
2
- * @file code-tokenizer.js
3
- * @module lumina/parsers/code-tokenizer
4
1
  *
5
- * Lightweight hand-rolled lexer for code-block syntax highlighting inside
6
- * AI Rich Responses. Returns two parallel representations:
7
2
  *
8
- * - `codeBlock` — array of `{ codeContent, highlightType }` where
9
- * `highlightType` is the numeric 0-5 enum used by WAProto.
10
- * - `unifiedBlocks` — array of `{ content, type }` with the string label
11
- * (`'DEFAULT' | 'KEYWORD' | 'METHOD' | 'STR' | 'NUMBER' | 'COMMENT'`).
12
3
  *
13
- * The legacy `AIRich.tokenizer` rebuilt its keyword Set on every call and
14
- * inlined all 660 lines of language maps. Lumina splits the catalog into
15
- * `code-tokenizer-keywords.js` (built once) and keeps this file focused on
16
- * the lexer itself (~120 lines).
17
- */
18
4
 
19
5
  import { HighlightType, HighlightLabel } from '../proto/enums.js'
20
6
  import { KEYWORDS, SLASH_COMMENT_LANGS, HASH_COMMENT_LANGS, BLOCK_COMMENT_LANGS } from './code-tokenizer-keywords.js'
21
7
 
22
- /**
23
- * @typedef {object} TokenizedCode
24
- * @property {Array<{ codeContent: string, highlightType: number }>} codeBlock
25
- * @property {Array<{ content: string, type: string }>} unifiedBlocks
26
- */
27
8
 
28
- /** Identifier character class per language (CSS & HTML allow `-` / `:`). */
29
9
  function identifierChar(lang) {
30
10
  switch (lang) {
31
11
  case 'css':
@@ -37,19 +17,12 @@ function identifierChar(lang) {
37
17
  }
38
18
  }
39
19
 
40
- /**
41
- * Tokenize a code string for syntax highlighting.
42
20
  *
43
- * @param {string} code
44
- * @param {string} [lang='javascript'] Lower-case language id. Unknown ids degrade gracefully to plain text.
45
- * @returns {TokenizedCode}
46
- */
47
21
  export function tokenizeCode(code, lang = 'javascript') {
48
22
  if (typeof code !== 'string' || code.length === 0) {
49
23
  return { codeBlock: [], unifiedBlocks: [] }
50
24
  }
51
25
 
52
- // Plain-text fast-path.
53
26
  if (!lang || lang === 'txt' || lang === 'text' || lang === 'plaintext') {
54
27
  return {
55
28
  codeBlock: [{ codeContent: code, highlightType: HighlightType.DEFAULT }],
@@ -64,12 +37,8 @@ export function tokenizeCode(code, lang = 'javascript') {
64
37
  const supportsHashComments = HASH_COMMENT_LANGS.has(lower)
65
38
  const supportsBlockComments = BLOCK_COMMENT_LANGS.has(lower)
66
39
 
67
- /** @type {Array<{ codeContent: string, highlightType: number }>} */
68
40
  const tokens = []
69
41
 
70
- /**
71
- * Push a token, merging with the previous one if both share the same type.
72
- */
73
42
  const push = (content, type) => {
74
43
  if (!content) return
75
44
  const last = tokens[tokens.length - 1]
@@ -81,7 +50,6 @@ export function tokenizeCode(code, lang = 'javascript') {
81
50
  while (i < code.length) {
82
51
  const c = code[i]
83
52
 
84
- // Whitespace.
85
53
  if (/\s/.test(c)) {
86
54
  const start = i
87
55
  while (i < code.length && /\s/.test(code[i])) i++
@@ -89,7 +57,6 @@ export function tokenizeCode(code, lang = 'javascript') {
89
57
  continue
90
58
  }
91
59
 
92
- // Block comment: /* ... */
93
60
  if (supportsBlockComments && c === '/' && code[i + 1] === '*') {
94
61
  const start = i
95
62
  i += 2
@@ -99,7 +66,6 @@ export function tokenizeCode(code, lang = 'javascript') {
99
66
  continue
100
67
  }
101
68
 
102
- // Slash line comment: //
103
69
  if (supportsSlashComments && c === '/' && code[i + 1] === '/') {
104
70
  const start = i
105
71
  while (i < code.length && code[i] !== '\n') i++
@@ -107,7 +73,6 @@ export function tokenizeCode(code, lang = 'javascript') {
107
73
  continue
108
74
  }
109
75
 
110
- // Hash line comment: # (python, bash, php, rust)
111
76
  if (supportsHashComments && c === '#') {
112
77
  const start = i
113
78
  while (i < code.length && code[i] !== '\n') i++
@@ -115,7 +80,6 @@ export function tokenizeCode(code, lang = 'javascript') {
115
80
  continue
116
81
  }
117
82
 
118
- // String literals: ', ", `
119
83
  if (c === '"' || c === "'" || c === '`') {
120
84
  const start = i
121
85
  const quote = c
@@ -131,7 +95,6 @@ export function tokenizeCode(code, lang = 'javascript') {
131
95
  continue
132
96
  }
133
97
 
134
- // Numbers.
135
98
  if (/[0-9]/.test(c)) {
136
99
  const start = i
137
100
  while (i < code.length && /[0-9._a-fxA-fX]/.test(code[i])) i++
@@ -139,7 +102,6 @@ export function tokenizeCode(code, lang = 'javascript') {
139
102
  continue
140
103
  }
141
104
 
142
- // Identifiers & keywords.
143
105
  if (/[a-zA-Z_$]/.test(c)) {
144
106
  const start = i
145
107
  while (i < code.length && isIdent.test(code[i])) i++
@@ -150,12 +112,10 @@ export function tokenizeCode(code, lang = 'javascript') {
150
112
  if (keywords.has(word)) {
151
113
  type = HighlightType.KEYWORD
152
114
  } else if (lower === 'css') {
153
- // Property name? next non-space char is ':'.
154
115
  let j = i
155
116
  while (j < code.length && /\s/.test(code[j])) j++
156
117
  if (code[j] === ':') type = HighlightType.KEYWORD
157
118
  } else if (lower === 'html') {
158
- // Tag name? previous non-space char is '<' or '</'.
159
119
  let p = start - 1
160
120
  while (p >= 0 && /\s/.test(code[p])) p--
161
121
  if (code[p] === '<' || (code[p] === '/' && code[p - 1] === '<')) {
@@ -163,7 +123,6 @@ export function tokenizeCode(code, lang = 'javascript') {
163
123
  }
164
124
  }
165
125
 
166
- // Function call? Next non-space char is '('.
167
126
  if (type === HighlightType.DEFAULT) {
168
127
  let j = i
169
128
  while (j < code.length && /\s/.test(code[j])) j++
@@ -174,7 +133,6 @@ export function tokenizeCode(code, lang = 'javascript') {
174
133
  continue
175
134
  }
176
135
 
177
- // Everything else: punctuation.
178
136
  push(c, HighlightType.DEFAULT)
179
137
  i++
180
138
  }
@@ -1,10 +1,3 @@
1
- /**
2
- * @file parsers/index.js
3
- * @module lumina/parsers
4
- *
5
- * Barrel re-export for the parsers layer.
6
- */
7
-
8
1
  export { extractInlineEntities } from './inline-entity.js'
9
2
  export { tokenizeCode } from './code-tokenizer.js'
10
3
  export { toTableMetadata } from './table-metadata.js'