@scriptables/manifest
Version:
Utilities to generate, parse, and update manifest headers in Scriptable scripts.
108 lines (88 loc) • 3.27 kB
text/typescript
import {SCRIPT_HEADER_NOTICES} from './scriptHeaderNotices';
type HeaderParser<T> = (headerLines: string[]) => T;
interface ParseOptions {
maxHeaderLines?: number;
headerNotices?: string[];
excludeNotices?: boolean;
}
interface ParsedScriptResult<T = never> {
header: T | null;
headerContent: string;
content: string;
}
export function parse(script: string): ParsedScriptResult<never>;
export function parse(script: string, options: ParseOptions): ParsedScriptResult<never>;
export function parse<T>(script: string, headerParser: HeaderParser<T>, options?: ParseOptions): ParsedScriptResult<T>;
export function parse<T = never>(
script: string,
headerParserOrOptions?: HeaderParser<T> | ParseOptions,
options: ParseOptions = {},
): ParsedScriptResult<T> {
// Fast path: invalid script returns directly
if (script.length < 2 || script.charCodeAt(0) !== 47 || script.charCodeAt(1) !== 47) {
return {header: null, headerContent: '', content: script || ''};
}
// Standardize options
const opts = {
maxHeaderLines: 20,
headerNotices: SCRIPT_HEADER_NOTICES,
excludeNotices: false,
...(typeof headerParserOrOptions === 'function' ? options : headerParserOrOptions),
};
const lines = new Array<string>();
let pos = 0;
let noticeIdx = 0;
let hasValidHeader = !opts.headerNotices.length;
const maxPos = script.length;
const maxLines = opts.maxHeaderLines;
let lineStart = 0;
let lineCount = 0;
let headerEndPos = 0;
// Parse header lines
while (lineCount < maxLines && pos < maxPos) {
// Find end of current line
while (pos < maxPos && script.charCodeAt(pos) !== 10) pos++;
const line = script.slice(lineStart, pos).trim();
// Check if we've reached non-comment line
if (!line.startsWith('//')) {
headerEndPos = lineStart;
break;
}
// Check for header notices if they exist
if (
opts.headerNotices.length > 0 &&
noticeIdx < opts.headerNotices.length &&
line.includes(opts.headerNotices[noticeIdx])
) {
if (++noticeIdx === opts.headerNotices.length) hasValidHeader = true;
}
// When headerNotices is empty, collect all comment lines
// When headerNotices exists, only collect if we have a valid header or are in the process of validating
if (!opts.headerNotices.length || hasValidHeader || noticeIdx > 0) {
lines.push(line);
}
lineCount++;
pos++;
lineStart = pos;
}
// Only validate header when headerNotices is not empty
if (opts.headerNotices.length && !hasValidHeader) {
return {header: null, headerContent: '', content: script};
}
// If we didn't hit a non-comment line, the header ends at current position
if (!headerEndPos) {
headerEndPos = pos;
}
const result: ParsedScriptResult<T> = {
header: null,
headerContent: lines.join('\n'),
content: script.slice(headerEndPos).replace(/^\n+/, ''),
};
// Parse header if parser is provided and we have lines to parse
if (typeof headerParserOrOptions === 'function' && lines.length) {
const headerLines =
opts.excludeNotices && opts.headerNotices.length ? lines.slice(opts.headerNotices.length) : lines;
result.header = headerParserOrOptions(headerLines);
}
return result;
}