Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 189 additions & 43 deletions lib/sugarValidatorLib.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,55 +160,201 @@ function validate(fileData, localStorage = {}) {
}
}

function findLastIfOpeningIndex(html) {
let maxIndex = html.length;
while (true) {
const index = html.substr(0, maxIndex).lastIndexOf('<<if');
if (index === -1) {
return -1;
function hiderTag(stack, tag) {
var hider = [];
while (stack.length != 0) {
const last = stack[stack.length-1];
if (last.tag != tag) {
hider.push(last);
stack.pop();
continue;
}
const nextChar = html[index + 4];
if (nextChar.match(/[^\w-]/)) {
return index;
} else {
maxIndex = index;
return hider[0];
}
return null;
}

function digStackForTag(stack, tag, subtype, pos, html, passage) {
if (subtype == 1) {
// html tag -> fake success
var fake = 1;
if (stack.length != 0) {
const last = stack[stack.length-1];
if (last.tag != tag || last.tagtype != subtype) {
addWarning([['Unmatched tag at ' + html.substring(pos, pos + 40)]], passage.header);
} else {
fake = 0;
}
}
if (fake) {
stack.push({});
}
return true;
}
while (stack.length != 0) {
const last = stack[stack.length-1];
if (last.tag != tag || last.tagtype != subtype) {
if (last.tagtype != 0) {
addWarning([['Unmatched tag at ' + html.substring(last.pos, last.pos + 40)]], passage.header);
stack.pop();
continue;
}
break;
}
return true;
}
return false;
}

function matchIfs(html, passage) {

function matchTags(html, passage) {
const htmlTags = ['div', 'b', 'strong', 'strike', 'u', 'i', 'li', 'ul', 'h1', 'h2', 'h3', 'p', 'table', 'tbody', 'th', 'tr', 'td', 'label', 'span', 'a', 'link', 'button', 'center'];
const sugarMacros = ['if', 'elseif', 'else', 'switch', 'case', 'default', 'for', 'do', 'nobr', 'silent', 'type', 'button', 'cycle', 'linkappend', 'linkprepend', 'linkreplace', 'link', 'listbox', 'append', 'prepend', 'replace', 'createaudiogroup', 'createplaylist', 'done', 'repeat', 'timed', 'widget', 'script',
'silently', 'click', 'endsilently', 'endclick',
'endif', 'endnobr','endfor', 'endscript', 'endbutton', 'endappend', 'endprepend', 'endreplace', 'endwidget'];
var cursor = 0;
var stack = [];
while(true) {
const start = findLastIfOpeningIndex(html);
// If there is no <<if
if (start === -1) {
// But there's still an /if, elseif or else, something's fucked
if (html.indexOf('<</if>>') !== -1) return throwError('Unmatched <</if>> found in passage', passage);
if (html.indexOf('<<elseif') !== -1) return throwError('Unmatched <<elseif found in passage', passage);
if (html.indexOf('<<else>>') !== -1) return throwError('Unmatched <<else>> found in passage', passage);
// If none of those still exist, we're good
return;
cursor = html.indexOf('<', cursor);
if (cursor === -1) {
digStackForTag(stack, '', -1, 0, html, passage);
const idx = stack.length-1;
if (idx >= 0) {
return throwError('Unmatched tag at ' + html.substring(stack[idx].pos, stack[idx].pos + 40), passage);
}
return; // done;
}
// If there still is an <<if
// Make sure there is also an <</if>>
const end1 = html.indexOf('<</if>>', start);
const end2 = html.indexOf('<<endif>>', start);
const end = (end2 === -1) ? end1 : (end1 === -1) ? end2 : Math.min(end1, end2);
if (end === -1) return throwError('Unmatched <<if found in passage ' + html.substr(start, 40), passage);
// Only look between the <<if and <</if>>
const endOfIf = html.indexOf('>>', start);
// But make sure our <<if isn't completely broken
if (endOfIf > end) return throwError('Malformed <<if found in passage', passage);
// Get content
const content = html.substring(endOfIf + 2, end);
// Check if there is an <<else>>
const elseIndex = content.indexOf('<<else>>', start);
// If so, make sure there isn't a another <<else>> or an <<elseif after the <<else>>
if (elseIndex !== -1) {
if (content.indexOf('<<else>>', elseIndex + 1) !== -1) return throwError('Double <<else>> found inside if block in passage', passage);
if (content.indexOf('<<elseif', elseIndex + 1) !== -1) return throwError('<<elseif found after <<else>> found inside if block in passage', passage);
cursor++;
var next, type = 0, subtype = 0;
var nextPos = -1;
if (html.charAt(cursor) == '<') {
cursor++;
// double opening -> possible SugarCube macro
if (html.charAt(cursor) == '/') {
cursor++;
// macro closing
type = 1;
}
const current = html.substring(cursor, cursor + 32);
for (const tag of sugarMacros) {
if (current.startsWith(tag)) {
const nextChar = current.charAt(tag.length);
if (nextChar.match(/[^\w-]/)) {
next = tag;
nextPos = cursor-2;
if (type != 0) {
nextPos--;
if (nextChar != '>' || current.charAt(tag.length + 1) != '>') {
return throwError('Broken closing macro at ' + html.substring(nextPos, nextPos + 40), passage);
}
}
break;
}
}
}
} else {
// single opening -> possible html tag
subtype = 1;
if (html.charAt(cursor) == '/') {
cursor++;
// tag closing
type = 1;
}
const current = html.substring(cursor, cursor + 32);
for (const tag of htmlTags) {
if (current.startsWith(tag)) {
const nextChar = current.charAt(tag.length);
if (nextChar.match(/[^\w-]/)) {
next = tag;
nextPos = cursor-1;
if (type != 0) {
nextPos--;
if (nextChar != '>') {
return throwError('Broken closing tag at ' + html.substring(nextPos, nextPos + 40), passage);
}
}
break;
}
}
}
}
if (nextPos === -1) {
continue;
}
// convert mid-macros
// -- deprecated closing macros
if (next.startsWith('end')) {
if (type === 1) {
return throwError('Invalid tag at ' + html.substring(nextPos, nextPos + 40), passage);
}
type = 1;
next = next.substring(3);
}
// -- mid macros of if statements
if (next == 'else' || next == 'elseif') {
if (type === 1) {
return throwError('Invalid tag at ' + html.substring(nextPos, nextPos + 40), passage);
}
type = next == 'else' ? -2 : -1;
next = 'if';
}
// -- mid macros of switch statements
if (next == 'case' || next == 'default') {
if (type === 1) {
return throwError('Invalid tag at ' + html.substring(nextPos, nextPos + 40), passage);
}
type = next == 'default' ? -4 : -3;
next = 'switch';
}
// update/check stack
if (type == 0) {
// standard opening tag -> add to stack
stack.push({tag:next, type:0, tagtype:subtype, pos: nextPos});
} else if (type < 0) {
// special sub-opening tags ('elseif' / 'else' , 'case' / 'default')
type = -type;
if (!digStackForTag(stack, next, subtype, nextPos, html, passage)) {
const hider = hiderTag(stack, next);
if (hider == null) {
if (type <= 2) {
// missing 'if'-branch
return throwError('Missing if-branch before ' + html.substring(nextPos, nextPos + 40), passage);
} else {
// missing 'switch'-branch
return throwError('Missing switch-branch before ' + html.substring(nextPos, nextPos + 40), passage);
}
} else {
// unmatched tags between (switch|case / case|default) or (if|elseif / else|elseif) tags
return throwError('Mangled tag at ' + html.substring(nextPos, nextPos + 40) + ', after unmatched tag at ' + html.substring(hider.pos, hider.pos + 40), passage);
}
}
const idx = stack.length-1;
if (stack[idx].type === 2 || stack[idx].type === 4) {
// 'else' / 'default' already found
if (type === 2) {
return throwError('Double <<else>> found inside if block at ' + html.substring(nextPos, nextPos + 40), passage);
} else if (type === 1) {
return throwError('<<elseif found after <<else>> inside if block at ' + html.substring(nextPos, nextPos + 40), passage);
} else if (type === 4) {
return throwError('Double <<default>> found inside switch block at ' + html.substring(nextPos, nextPos + 40), passage);
} else if (type === 3) {
return throwError('<<case found after <<default>> inside switch block at ' + html.substring(nextPos, nextPos + 40), passage);
}
}
stack[idx].type = type;
stack[idx].pos = nextPos;
} else {
// closing tags
if (!digStackForTag(stack, next, subtype, nextPos, html, passage)) {
const hider = hiderTag(stack, next);
if (hider == null) {
return throwError('Unmatched tag at ' + html.substring(nextPos, nextPos + 40), passage);
} else {
return throwError('Mangled tag at ' + html.substring(nextPos, nextPos + 40) + ', after unmatched tag at ' + html.substring(hider.pos, hider.pos + 40), passage);
}
}
stack.pop();
}
// Remove this if block from the HTML
html = html.substr(0, start) + html.substr(end + 7);
cursor = nextPos + 3;
}
}

Expand Down Expand Up @@ -523,7 +669,7 @@ function validate(fileData, localStorage = {}) {
try {
matchQuotes(passage.content, passage);
matchGTLT(passage.content, passage);
matchIfs(passage.content, passage);
matchTags(passage.content, passage);
findDeprecatedInPassage(passage.content, passage);
findInvalidConditions(passage.content, passage);
checkInvalidWidgets(passage.content, passage);
Expand Down