import {
CODE_POINTS as $,
getSurrogatePairCodePoint,
isControlCodePoint,
isSurrogate,
isSurrogatePair,
isUndefinedCodePoint,
} from '../common/unicode.js';
import { ERR, type ParserError, type ParserErrorHandler } from '../common/error-codes.js';
const DEFAULT_BUFFER_WATERLINE = 1 << 16;
export class Preprocessor {
public html = '';
public pos = -1;
private lastGapPos = -2;
private gapStack: number[] = [];
private skipNextNewLine = false;
public lastChunkWritten = false;
public endOfChunkHit = false;
public bufferWaterline = DEFAULT_BUFFER_WATERLINE;
private isEol = false;
private lineStartPos = 0;
public droppedBufferSize = 0;
public line = 1;
constructor(private handler: { onParseError?: ParserErrorHandler | null }) {}
public get col(): number {
return this.pos - this.lineStartPos + Number(this.lastGapPos !== this.pos);
}
public get offset(): number {
return this.droppedBufferSize + this.pos;
}
public getError(code: ERR, cpOffset: number): ParserError {
const { line, col, offset } = this;
const startCol = col + cpOffset;
const startOffset = offset + cpOffset;
return {
code,
startLine: line,
endLine: line,
startCol,
endCol: startCol,
startOffset,
endOffset: startOffset,
};
}
private lastErrOffset = -1;
private _err(code: ERR): void {
if (this.handler.onParseError && this.lastErrOffset !== this.offset) {
this.lastErrOffset = this.offset;
this.handler.onParseError(this.getError(code, 0));
}
}
private _addGap(): void {
this.gapStack.push(this.lastGapPos);
this.lastGapPos = this.pos;
}
private _processSurrogate(cp: number): number {
if (this.pos !== this.html.length - 1) {
const nextCp = this.html.charCodeAt(this.pos + 1);
if (isSurrogatePair(nextCp)) {
this.pos++;
this._addGap();
return getSurrogatePairCodePoint(cp, nextCp);
}
}
else if (!this.lastChunkWritten) {
this.endOfChunkHit = true;
return $.EOF;
}
this._err(ERR.surrogateInInputStream);
return cp;
}
public willDropParsedChunk(): boolean {
return this.pos > this.bufferWaterline;
}
public dropParsedChunk(): void {
if (this.willDropParsedChunk()) {
this.html = this.html.substring(this.pos);
this.lineStartPos -= this.pos;
this.droppedBufferSize += this.pos;
this.pos = 0;
this.lastGapPos = -2;
this.gapStack.length = 0;
}
}
public write(chunk: string, isLastChunk: boolean): void {
if (this.html.length > 0) {
this.html += chunk;
} else {
this.html = chunk;
}
this.endOfChunkHit = false;
this.lastChunkWritten = isLastChunk;
}
public insertHtmlAtCurrentPos(chunk: string): void {
this.html = this.html.substring(0, this.pos + 1) + chunk + this.html.substring(this.pos + 1);
this.endOfChunkHit = false;
}
public startsWith(pattern: string, caseSensitive: boolean): boolean {
if (this.pos + pattern.length > this.html.length) {
this.endOfChunkHit = !this.lastChunkWritten;
return false;
}
if (caseSensitive) {
return this.html.startsWith(pattern, this.pos);
}
for (let i = 0; i < pattern.length; i++) {
const cp = this.html.charCodeAt(this.pos + i) | 0x20;
if (cp !== pattern.charCodeAt(i)) {
return false;
}
}
return true;
}
public peek(offset: number): number {
const pos = this.pos + offset;
if (pos >= this.html.length) {
this.endOfChunkHit = !this.lastChunkWritten;
return $.EOF;
}
const code = this.html.charCodeAt(pos);
return code === $.CARRIAGE_RETURN ? $.LINE_FEED : code;
}
public advance(): number {
this.pos++;
if (this.isEol) {
this.isEol = false;
this.line++;
this.lineStartPos = this.pos;
}
if (this.pos >= this.html.length) {
this.endOfChunkHit = !this.lastChunkWritten;
return $.EOF;
}
let cp = this.html.charCodeAt(this.pos);
if (cp === $.CARRIAGE_RETURN) {
this.isEol = true;
this.skipNextNewLine = true;
return $.LINE_FEED;
}
if (cp === $.LINE_FEED) {
this.isEol = true;
if (this.skipNextNewLine) {
this.line--;
this.skipNextNewLine = false;
this._addGap();
return this.advance();
}
}
this.skipNextNewLine = false;
if (isSurrogate(cp)) {
cp = this._processSurrogate(cp);
}
const isCommonValidRange =
this.handler.onParseError === null ||
(cp > 0x1f && cp < 0x7f) ||
cp === $.LINE_FEED ||
cp === $.CARRIAGE_RETURN ||
(cp > 0x9f && cp < 0xfd_d0);
if (!isCommonValidRange) {
this._checkForProblematicCharacters(cp);
}
return cp;
}
private _checkForProblematicCharacters(cp: number): void {
if (isControlCodePoint(cp)) {
this._err(ERR.controlCharacterInInputStream);
} else if (isUndefinedCodePoint(cp)) {
this._err(ERR.noncharacterInInputStream);
}
}
public retreat(count: number): void {
this.pos -= count;
while (this.pos < this.lastGapPos) {
this.lastGapPos = this.gapStack.pop()!;
this.pos--;
}
this.isEol = false;
}
}