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.
612 lines
14 KiB
JavaScript
612 lines
14 KiB
JavaScript
'use strict'
|
|
|
|
let AtRule = require('./at-rule')
|
|
let Comment = require('./comment')
|
|
let Declaration = require('./declaration')
|
|
let Root = require('./root')
|
|
let Rule = require('./rule')
|
|
let tokenizer = require('./tokenize')
|
|
|
|
const SAFE_COMMENT_NEIGHBOR = {
|
|
empty: true,
|
|
space: true
|
|
}
|
|
|
|
function findLastWithPosition(tokens) {
|
|
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
let token = tokens[i]
|
|
let pos = token[3] || token[2]
|
|
if (pos) return pos
|
|
}
|
|
}
|
|
|
|
class Parser {
|
|
constructor(input) {
|
|
this.input = input
|
|
|
|
this.root = new Root()
|
|
this.current = this.root
|
|
this.spaces = ''
|
|
this.semicolon = false
|
|
|
|
this.createTokenizer()
|
|
this.root.source = { input, start: { column: 1, line: 1, offset: 0 } }
|
|
}
|
|
|
|
atrule(token) {
|
|
let node = new AtRule()
|
|
node.name = token[1].slice(1)
|
|
if (node.name === '') {
|
|
this.unnamedAtrule(node, token)
|
|
}
|
|
this.init(node, token[2])
|
|
|
|
let type
|
|
let prev
|
|
let shift
|
|
let last = false
|
|
let open = false
|
|
let params = []
|
|
let brackets = []
|
|
|
|
while (!this.tokenizer.endOfFile()) {
|
|
token = this.tokenizer.nextToken()
|
|
type = token[0]
|
|
|
|
if (type === '(' || type === '[') {
|
|
brackets.push(type === '(' ? ')' : ']')
|
|
} else if (type === '{' && brackets.length > 0) {
|
|
brackets.push('}')
|
|
} else if (type === brackets[brackets.length - 1]) {
|
|
brackets.pop()
|
|
}
|
|
|
|
if (brackets.length === 0) {
|
|
if (type === ';') {
|
|
node.source.end = this.getPosition(token[2])
|
|
node.source.end.offset++
|
|
this.semicolon = true
|
|
break
|
|
} else if (type === '{') {
|
|
open = true
|
|
break
|
|
} else if (type === '}') {
|
|
if (params.length > 0) {
|
|
shift = params.length - 1
|
|
prev = params[shift]
|
|
while (prev && prev[0] === 'space') {
|
|
prev = params[--shift]
|
|
}
|
|
if (prev) {
|
|
node.source.end = this.getPosition(prev[3] || prev[2])
|
|
node.source.end.offset++
|
|
}
|
|
}
|
|
this.end(token)
|
|
break
|
|
} else {
|
|
params.push(token)
|
|
}
|
|
} else {
|
|
params.push(token)
|
|
}
|
|
|
|
if (this.tokenizer.endOfFile()) {
|
|
last = true
|
|
break
|
|
}
|
|
}
|
|
|
|
node.raws.between = this.spacesAndCommentsFromEnd(params)
|
|
if (params.length) {
|
|
node.raws.afterName = this.spacesAndCommentsFromStart(params)
|
|
this.raw(node, 'params', params)
|
|
if (last) {
|
|
token = params[params.length - 1]
|
|
node.source.end = this.getPosition(token[3] || token[2])
|
|
node.source.end.offset++
|
|
this.spaces = node.raws.between
|
|
node.raws.between = ''
|
|
}
|
|
} else {
|
|
node.raws.afterName = ''
|
|
node.params = ''
|
|
}
|
|
|
|
if (open) {
|
|
node.nodes = []
|
|
this.current = node
|
|
}
|
|
}
|
|
|
|
checkMissedSemicolon(tokens) {
|
|
let colon = this.colon(tokens)
|
|
if (colon === false) return
|
|
|
|
let founded = 0
|
|
let token
|
|
for (let j = colon - 1; j >= 0; j--) {
|
|
token = tokens[j]
|
|
if (token[0] !== 'space') {
|
|
founded += 1
|
|
if (founded === 2) break
|
|
}
|
|
}
|
|
// If the token is a word, e.g. `!important`, `red` or any other valid property's value.
|
|
// Then we need to return the colon after that word token. [3] is the "end" colon of that word.
|
|
// And because we need it after that one we do +1 to get the next one.
|
|
throw this.input.error(
|
|
'Missed semicolon',
|
|
token[0] === 'word' ? token[3] + 1 : token[2]
|
|
)
|
|
}
|
|
|
|
colon(tokens) {
|
|
let brackets = 0
|
|
let prev, token, type
|
|
for (let [i, element] of tokens.entries()) {
|
|
token = element
|
|
type = token[0]
|
|
|
|
if (type === '(') {
|
|
brackets += 1
|
|
}
|
|
if (type === ')') {
|
|
brackets -= 1
|
|
}
|
|
if (brackets === 0 && type === ':') {
|
|
if (!prev) {
|
|
this.doubleColon(token)
|
|
} else if (prev[0] === 'word' && prev[1] === 'progid') {
|
|
continue
|
|
} else {
|
|
return i
|
|
}
|
|
}
|
|
|
|
prev = token
|
|
}
|
|
return false
|
|
}
|
|
|
|
comment(token) {
|
|
let node = new Comment()
|
|
this.init(node, token[2])
|
|
node.source.end = this.getPosition(token[3] || token[2])
|
|
node.source.end.offset++
|
|
|
|
let text = token[1].slice(2, -2)
|
|
if (/^\s*$/.test(text)) {
|
|
node.text = ''
|
|
node.raws.left = text
|
|
node.raws.right = ''
|
|
} else {
|
|
let match = text.match(/^(\s*)([^]*\S)(\s*)$/)
|
|
node.text = match[2]
|
|
node.raws.left = match[1]
|
|
node.raws.right = match[3]
|
|
}
|
|
}
|
|
|
|
createTokenizer() {
|
|
this.tokenizer = tokenizer(this.input)
|
|
}
|
|
|
|
decl(tokens, customProperty) {
|
|
let node = new Declaration()
|
|
this.init(node, tokens[0][2])
|
|
|
|
let last = tokens[tokens.length - 1]
|
|
if (last[0] === ';') {
|
|
this.semicolon = true
|
|
tokens.pop()
|
|
}
|
|
|
|
node.source.end = this.getPosition(
|
|
last[3] || last[2] || findLastWithPosition(tokens)
|
|
)
|
|
node.source.end.offset++
|
|
|
|
while (tokens[0][0] !== 'word') {
|
|
if (tokens.length === 1) this.unknownWord(tokens)
|
|
node.raws.before += tokens.shift()[1]
|
|
}
|
|
node.source.start = this.getPosition(tokens[0][2])
|
|
|
|
node.prop = ''
|
|
while (tokens.length) {
|
|
let type = tokens[0][0]
|
|
if (type === ':' || type === 'space' || type === 'comment') {
|
|
break
|
|
}
|
|
node.prop += tokens.shift()[1]
|
|
}
|
|
|
|
node.raws.between = ''
|
|
|
|
let token
|
|
while (tokens.length) {
|
|
token = tokens.shift()
|
|
|
|
if (token[0] === ':') {
|
|
node.raws.between += token[1]
|
|
break
|
|
} else {
|
|
if (token[0] === 'word' && /\w/.test(token[1])) {
|
|
this.unknownWord([token])
|
|
}
|
|
node.raws.between += token[1]
|
|
}
|
|
}
|
|
|
|
if (node.prop[0] === '_' || node.prop[0] === '*') {
|
|
node.raws.before += node.prop[0]
|
|
node.prop = node.prop.slice(1)
|
|
}
|
|
|
|
let firstSpaces = []
|
|
let next
|
|
while (tokens.length) {
|
|
next = tokens[0][0]
|
|
if (next !== 'space' && next !== 'comment') break
|
|
firstSpaces.push(tokens.shift())
|
|
}
|
|
|
|
this.precheckMissedSemicolon(tokens)
|
|
|
|
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
token = tokens[i]
|
|
if (token[1].toLowerCase() === '!important') {
|
|
node.important = true
|
|
let string = this.stringFrom(tokens, i)
|
|
string = this.spacesFromEnd(tokens) + string
|
|
if (string !== ' !important') node.raws.important = string
|
|
break
|
|
} else if (token[1].toLowerCase() === 'important') {
|
|
let cache = tokens.slice(0)
|
|
let str = ''
|
|
for (let j = i; j > 0; j--) {
|
|
let type = cache[j][0]
|
|
if (str.trim().startsWith('!') && type !== 'space') {
|
|
break
|
|
}
|
|
str = cache.pop()[1] + str
|
|
}
|
|
if (str.trim().startsWith('!')) {
|
|
node.important = true
|
|
node.raws.important = str
|
|
tokens = cache
|
|
}
|
|
}
|
|
|
|
if (token[0] !== 'space' && token[0] !== 'comment') {
|
|
break
|
|
}
|
|
}
|
|
|
|
let hasWord = tokens.some(i => i[0] !== 'space' && i[0] !== 'comment')
|
|
|
|
if (hasWord) {
|
|
node.raws.between += firstSpaces.map(i => i[1]).join('')
|
|
firstSpaces = []
|
|
}
|
|
this.raw(node, 'value', firstSpaces.concat(tokens), customProperty)
|
|
|
|
if (node.value.includes(':') && !customProperty) {
|
|
this.checkMissedSemicolon(tokens)
|
|
}
|
|
}
|
|
|
|
doubleColon(token) {
|
|
throw this.input.error(
|
|
'Double colon',
|
|
{ offset: token[2] },
|
|
{ offset: token[2] + token[1].length }
|
|
)
|
|
}
|
|
|
|
emptyRule(token) {
|
|
let node = new Rule()
|
|
this.init(node, token[2])
|
|
node.selector = ''
|
|
node.raws.between = ''
|
|
this.current = node
|
|
}
|
|
|
|
end(token) {
|
|
if (this.current.nodes && this.current.nodes.length) {
|
|
this.current.raws.semicolon = this.semicolon
|
|
}
|
|
this.semicolon = false
|
|
|
|
this.current.raws.after = (this.current.raws.after || '') + this.spaces
|
|
this.spaces = ''
|
|
|
|
if (this.current.parent) {
|
|
this.current.source.end = this.getPosition(token[2])
|
|
this.current.source.end.offset++
|
|
this.current = this.current.parent
|
|
} else {
|
|
this.unexpectedClose(token)
|
|
}
|
|
}
|
|
|
|
endFile() {
|
|
if (this.current.parent) this.unclosedBlock()
|
|
if (this.current.nodes && this.current.nodes.length) {
|
|
this.current.raws.semicolon = this.semicolon
|
|
}
|
|
this.current.raws.after = (this.current.raws.after || '') + this.spaces
|
|
this.root.source.end = this.getPosition(this.tokenizer.position())
|
|
}
|
|
|
|
freeSemicolon(token) {
|
|
this.spaces += token[1]
|
|
if (this.current.nodes) {
|
|
let prev = this.current.nodes[this.current.nodes.length - 1]
|
|
if (prev && prev.type === 'rule' && !prev.raws.ownSemicolon) {
|
|
prev.raws.ownSemicolon = this.spaces
|
|
this.spaces = ''
|
|
prev.source.end = this.getPosition(token[2])
|
|
prev.source.end.offset += prev.raws.ownSemicolon.length
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helpers
|
|
|
|
getPosition(offset) {
|
|
let pos = this.input.fromOffset(offset)
|
|
return {
|
|
column: pos.col,
|
|
line: pos.line,
|
|
offset
|
|
}
|
|
}
|
|
|
|
init(node, offset) {
|
|
this.current.push(node)
|
|
node.source = {
|
|
input: this.input,
|
|
start: this.getPosition(offset)
|
|
}
|
|
node.raws.before = this.spaces
|
|
this.spaces = ''
|
|
if (node.type !== 'comment') this.semicolon = false
|
|
}
|
|
|
|
other(start) {
|
|
let end = false
|
|
let type = null
|
|
let colon = false
|
|
let bracket = null
|
|
let brackets = []
|
|
let customProperty = start[1].startsWith('--')
|
|
|
|
let tokens = []
|
|
let token = start
|
|
while (token) {
|
|
type = token[0]
|
|
tokens.push(token)
|
|
|
|
if (type === '(' || type === '[') {
|
|
if (!bracket) bracket = token
|
|
brackets.push(type === '(' ? ')' : ']')
|
|
} else if (customProperty && colon && type === '{') {
|
|
if (!bracket) bracket = token
|
|
brackets.push('}')
|
|
} else if (brackets.length === 0) {
|
|
if (type === ';') {
|
|
if (colon) {
|
|
this.decl(tokens, customProperty)
|
|
return
|
|
} else {
|
|
break
|
|
}
|
|
} else if (type === '{') {
|
|
this.rule(tokens)
|
|
return
|
|
} else if (type === '}') {
|
|
this.tokenizer.back(tokens.pop())
|
|
end = true
|
|
break
|
|
} else if (type === ':') {
|
|
colon = true
|
|
}
|
|
} else if (type === brackets[brackets.length - 1]) {
|
|
brackets.pop()
|
|
if (brackets.length === 0) bracket = null
|
|
}
|
|
|
|
token = this.tokenizer.nextToken()
|
|
}
|
|
|
|
if (this.tokenizer.endOfFile()) end = true
|
|
if (brackets.length > 0) this.unclosedBracket(bracket)
|
|
|
|
if (end && colon) {
|
|
if (!customProperty) {
|
|
while (tokens.length) {
|
|
token = tokens[tokens.length - 1][0]
|
|
if (token !== 'space' && token !== 'comment') break
|
|
this.tokenizer.back(tokens.pop())
|
|
}
|
|
}
|
|
this.decl(tokens, customProperty)
|
|
} else {
|
|
this.unknownWord(tokens)
|
|
}
|
|
}
|
|
|
|
parse() {
|
|
let token
|
|
while (!this.tokenizer.endOfFile()) {
|
|
token = this.tokenizer.nextToken()
|
|
|
|
switch (token[0]) {
|
|
case 'space':
|
|
this.spaces += token[1]
|
|
break
|
|
|
|
case ';':
|
|
this.freeSemicolon(token)
|
|
break
|
|
|
|
case '}':
|
|
this.end(token)
|
|
break
|
|
|
|
case 'comment':
|
|
this.comment(token)
|
|
break
|
|
|
|
case 'at-word':
|
|
this.atrule(token)
|
|
break
|
|
|
|
case '{':
|
|
this.emptyRule(token)
|
|
break
|
|
|
|
default:
|
|
this.other(token)
|
|
break
|
|
}
|
|
}
|
|
this.endFile()
|
|
}
|
|
|
|
precheckMissedSemicolon(/* tokens */) {
|
|
// Hook for Safe Parser
|
|
}
|
|
|
|
raw(node, prop, tokens, customProperty) {
|
|
let token, type
|
|
let length = tokens.length
|
|
let value = ''
|
|
let clean = true
|
|
let next, prev
|
|
|
|
for (let i = 0; i < length; i += 1) {
|
|
token = tokens[i]
|
|
type = token[0]
|
|
if (type === 'space' && i === length - 1 && !customProperty) {
|
|
clean = false
|
|
} else if (type === 'comment') {
|
|
prev = tokens[i - 1] ? tokens[i - 1][0] : 'empty'
|
|
next = tokens[i + 1] ? tokens[i + 1][0] : 'empty'
|
|
if (!SAFE_COMMENT_NEIGHBOR[prev] && !SAFE_COMMENT_NEIGHBOR[next]) {
|
|
if (value.slice(-1) === ',') {
|
|
clean = false
|
|
} else {
|
|
value += token[1]
|
|
}
|
|
} else {
|
|
clean = false
|
|
}
|
|
} else {
|
|
value += token[1]
|
|
}
|
|
}
|
|
if (!clean) {
|
|
let raw = tokens.reduce((all, i) => all + i[1], '')
|
|
node.raws[prop] = { raw, value }
|
|
}
|
|
node[prop] = value
|
|
}
|
|
|
|
rule(tokens) {
|
|
tokens.pop()
|
|
|
|
let node = new Rule()
|
|
this.init(node, tokens[0][2])
|
|
|
|
node.raws.between = this.spacesAndCommentsFromEnd(tokens)
|
|
this.raw(node, 'selector', tokens)
|
|
this.current = node
|
|
}
|
|
|
|
spacesAndCommentsFromEnd(tokens) {
|
|
let lastTokenType
|
|
let spaces = ''
|
|
while (tokens.length) {
|
|
lastTokenType = tokens[tokens.length - 1][0]
|
|
if (lastTokenType !== 'space' && lastTokenType !== 'comment') break
|
|
spaces = tokens.pop()[1] + spaces
|
|
}
|
|
return spaces
|
|
}
|
|
|
|
// Errors
|
|
|
|
spacesAndCommentsFromStart(tokens) {
|
|
let next
|
|
let spaces = ''
|
|
while (tokens.length) {
|
|
next = tokens[0][0]
|
|
if (next !== 'space' && next !== 'comment') break
|
|
spaces += tokens.shift()[1]
|
|
}
|
|
return spaces
|
|
}
|
|
|
|
spacesFromEnd(tokens) {
|
|
let lastTokenType
|
|
let spaces = ''
|
|
while (tokens.length) {
|
|
lastTokenType = tokens[tokens.length - 1][0]
|
|
if (lastTokenType !== 'space') break
|
|
spaces = tokens.pop()[1] + spaces
|
|
}
|
|
return spaces
|
|
}
|
|
|
|
stringFrom(tokens, from) {
|
|
let result = ''
|
|
for (let i = from; i < tokens.length; i++) {
|
|
result += tokens[i][1]
|
|
}
|
|
tokens.splice(from, tokens.length - from)
|
|
return result
|
|
}
|
|
|
|
unclosedBlock() {
|
|
let pos = this.current.source.start
|
|
throw this.input.error('Unclosed block', pos.line, pos.column)
|
|
}
|
|
|
|
unclosedBracket(bracket) {
|
|
throw this.input.error(
|
|
'Unclosed bracket',
|
|
{ offset: bracket[2] },
|
|
{ offset: bracket[2] + 1 }
|
|
)
|
|
}
|
|
|
|
unexpectedClose(token) {
|
|
throw this.input.error(
|
|
'Unexpected }',
|
|
{ offset: token[2] },
|
|
{ offset: token[2] + 1 }
|
|
)
|
|
}
|
|
|
|
unknownWord(tokens) {
|
|
throw this.input.error(
|
|
'Unknown word ' + tokens[0][1],
|
|
{ offset: tokens[0][2] },
|
|
{ offset: tokens[0][2] + tokens[0][1].length }
|
|
)
|
|
}
|
|
|
|
unnamedAtrule(node, token) {
|
|
throw this.input.error(
|
|
'At-rule without name',
|
|
{ offset: token[2] },
|
|
{ offset: token[2] + token[1].length }
|
|
)
|
|
}
|
|
}
|
|
|
|
module.exports = Parser
|