forked from FINAKON/HelpProject
1. Initial Commit - a boiler plate code and POC to realize the concept of context sensitive help 2. Frontend code written in ReactJS 3. Backend code written in Java, Spring Boot Framework 4. Frontend Start: pre-requisites : node, npm npm run dev ==> to start the frontend vite server 5. Backend Start: pre-requisites : java, mvn mvn spring-boot:run ==> to start the backend server 6. Visit http://localhost:5173/ for basic demo of help, press F1 in textboxes 7. Visit http://localhost:5173/editor and enter "admin123" to add/modify texts. Happy Coding !!! Thank you, Bhargava.
197 lines
6.9 KiB
JavaScript
197 lines
6.9 KiB
JavaScript
import { CODE_POINTS as $, getSurrogatePairCodePoint, isControlCodePoint, isSurrogate, isSurrogatePair, isUndefinedCodePoint, } from '../common/unicode.js';
|
|
import { ERR } from '../common/error-codes.js';
|
|
//Const
|
|
const DEFAULT_BUFFER_WATERLINE = 1 << 16;
|
|
//Preprocessor
|
|
//NOTE: HTML input preprocessing
|
|
//(see: http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html#preprocessing-the-input-stream)
|
|
export class Preprocessor {
|
|
constructor(handler) {
|
|
this.handler = handler;
|
|
this.html = '';
|
|
this.pos = -1;
|
|
// NOTE: Initial `lastGapPos` is -2, to ensure `col` on initialisation is 0
|
|
this.lastGapPos = -2;
|
|
this.gapStack = [];
|
|
this.skipNextNewLine = false;
|
|
this.lastChunkWritten = false;
|
|
this.endOfChunkHit = false;
|
|
this.bufferWaterline = DEFAULT_BUFFER_WATERLINE;
|
|
this.isEol = false;
|
|
this.lineStartPos = 0;
|
|
this.droppedBufferSize = 0;
|
|
this.line = 1;
|
|
//NOTE: avoid reporting errors twice on advance/retreat
|
|
this.lastErrOffset = -1;
|
|
}
|
|
/** The column on the current line. If we just saw a gap (eg. a surrogate pair), return the index before. */
|
|
get col() {
|
|
return this.pos - this.lineStartPos + Number(this.lastGapPos !== this.pos);
|
|
}
|
|
get offset() {
|
|
return this.droppedBufferSize + this.pos;
|
|
}
|
|
getError(code, cpOffset) {
|
|
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,
|
|
};
|
|
}
|
|
_err(code) {
|
|
if (this.handler.onParseError && this.lastErrOffset !== this.offset) {
|
|
this.lastErrOffset = this.offset;
|
|
this.handler.onParseError(this.getError(code, 0));
|
|
}
|
|
}
|
|
_addGap() {
|
|
this.gapStack.push(this.lastGapPos);
|
|
this.lastGapPos = this.pos;
|
|
}
|
|
_processSurrogate(cp) {
|
|
//NOTE: try to peek a surrogate pair
|
|
if (this.pos !== this.html.length - 1) {
|
|
const nextCp = this.html.charCodeAt(this.pos + 1);
|
|
if (isSurrogatePair(nextCp)) {
|
|
//NOTE: we have a surrogate pair. Peek pair character and recalculate code point.
|
|
this.pos++;
|
|
//NOTE: add a gap that should be avoided during retreat
|
|
this._addGap();
|
|
return getSurrogatePairCodePoint(cp, nextCp);
|
|
}
|
|
}
|
|
//NOTE: we are at the end of a chunk, therefore we can't infer the surrogate pair yet.
|
|
else if (!this.lastChunkWritten) {
|
|
this.endOfChunkHit = true;
|
|
return $.EOF;
|
|
}
|
|
//NOTE: isolated surrogate
|
|
this._err(ERR.surrogateInInputStream);
|
|
return cp;
|
|
}
|
|
willDropParsedChunk() {
|
|
return this.pos > this.bufferWaterline;
|
|
}
|
|
dropParsedChunk() {
|
|
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;
|
|
}
|
|
}
|
|
write(chunk, isLastChunk) {
|
|
if (this.html.length > 0) {
|
|
this.html += chunk;
|
|
}
|
|
else {
|
|
this.html = chunk;
|
|
}
|
|
this.endOfChunkHit = false;
|
|
this.lastChunkWritten = isLastChunk;
|
|
}
|
|
insertHtmlAtCurrentPos(chunk) {
|
|
this.html = this.html.substring(0, this.pos + 1) + chunk + this.html.substring(this.pos + 1);
|
|
this.endOfChunkHit = false;
|
|
}
|
|
startsWith(pattern, caseSensitive) {
|
|
// Check if our buffer has enough characters
|
|
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;
|
|
}
|
|
peek(offset) {
|
|
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;
|
|
}
|
|
advance() {
|
|
this.pos++;
|
|
//NOTE: LF should be in the last column of the line
|
|
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);
|
|
//NOTE: all U+000D CARRIAGE RETURN (CR) characters must be converted to U+000A LINE FEED (LF) characters
|
|
if (cp === $.CARRIAGE_RETURN) {
|
|
this.isEol = true;
|
|
this.skipNextNewLine = true;
|
|
return $.LINE_FEED;
|
|
}
|
|
//NOTE: any U+000A LINE FEED (LF) characters that immediately follow a U+000D CARRIAGE RETURN (CR) character
|
|
//must be ignored.
|
|
if (cp === $.LINE_FEED) {
|
|
this.isEol = true;
|
|
if (this.skipNextNewLine) {
|
|
// `line` will be bumped again in the recursive call.
|
|
this.line--;
|
|
this.skipNextNewLine = false;
|
|
this._addGap();
|
|
return this.advance();
|
|
}
|
|
}
|
|
this.skipNextNewLine = false;
|
|
if (isSurrogate(cp)) {
|
|
cp = this._processSurrogate(cp);
|
|
}
|
|
//OPTIMIZATION: first check if code point is in the common allowed
|
|
//range (ASCII alphanumeric, whitespaces, big chunk of BMP)
|
|
//before going into detailed performance cost validation.
|
|
const isCommonValidRange = this.handler.onParseError === null ||
|
|
(cp > 0x1f && cp < 0x7f) ||
|
|
cp === $.LINE_FEED ||
|
|
cp === $.CARRIAGE_RETURN ||
|
|
(cp > 0x9f && cp < 64976);
|
|
if (!isCommonValidRange) {
|
|
this._checkForProblematicCharacters(cp);
|
|
}
|
|
return cp;
|
|
}
|
|
_checkForProblematicCharacters(cp) {
|
|
if (isControlCodePoint(cp)) {
|
|
this._err(ERR.controlCharacterInInputStream);
|
|
}
|
|
else if (isUndefinedCodePoint(cp)) {
|
|
this._err(ERR.noncharacterInInputStream);
|
|
}
|
|
}
|
|
retreat(count) {
|
|
this.pos -= count;
|
|
while (this.pos < this.lastGapPos) {
|
|
this.lastGapPos = this.gapStack.pop();
|
|
this.pos--;
|
|
}
|
|
this.isEol = false;
|
|
}
|
|
}
|