Bhargava 6063bd1724 Help Project:
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.
2025-07-04 15:54:13 +05:30

338 lines
8.3 KiB
JavaScript

/**
* @import {ElementData, Element, Nodes, RootContent, Root} from 'hast'
* @import {DefaultTreeAdapterMap, Token} from 'parse5'
* @import {Schema} from 'property-information'
* @import {Point, Position} from 'unist'
* @import {VFile} from 'vfile'
* @import {Options} from 'hast-util-from-parse5'
*/
/**
* @typedef State
* Info passed around about the current state.
* @property {VFile | undefined} file
* Corresponding file.
* @property {boolean} location
* Whether location info was found.
* @property {Schema} schema
* Current schema.
* @property {boolean | undefined} verbose
* Add extra positional info.
*/
import {ok as assert} from 'devlop'
import {h, s} from 'hastscript'
import {find, html, svg} from 'property-information'
import {location} from 'vfile-location'
import {webNamespaces} from 'web-namespaces'
const own = {}.hasOwnProperty
/** @type {unknown} */
// type-coverage:ignore-next-line
const proto = Object.prototype
/**
* Transform a `parse5` AST to hast.
*
* @param {DefaultTreeAdapterMap['node']} tree
* `parse5` tree to transform.
* @param {Options | null | undefined} [options]
* Configuration (optional).
* @returns {Nodes}
* hast tree.
*/
export function fromParse5(tree, options) {
const settings = options || {}
return one(
{
file: settings.file || undefined,
location: false,
schema: settings.space === 'svg' ? svg : html,
verbose: settings.verbose || false
},
tree
)
}
/**
* Transform a node.
*
* @param {State} state
* Info passed around about the current state.
* @param {DefaultTreeAdapterMap['node']} node
* p5 node.
* @returns {Nodes}
* hast node.
*/
function one(state, node) {
/** @type {Nodes} */
let result
switch (node.nodeName) {
case '#comment': {
const reference = /** @type {DefaultTreeAdapterMap['commentNode']} */ (
node
)
result = {type: 'comment', value: reference.data}
patch(state, reference, result)
return result
}
case '#document':
case '#document-fragment': {
const reference =
/** @type {DefaultTreeAdapterMap['document'] | DefaultTreeAdapterMap['documentFragment']} */ (
node
)
const quirksMode =
'mode' in reference
? reference.mode === 'quirks' || reference.mode === 'limited-quirks'
: false
result = {
type: 'root',
children: all(state, node.childNodes),
data: {quirksMode}
}
if (state.file && state.location) {
const document = String(state.file)
const loc = location(document)
const start = loc.toPoint(0)
const end = loc.toPoint(document.length)
// Always defined as we give valid input.
assert(start, 'expected `start`')
assert(end, 'expected `end`')
result.position = {start, end}
}
return result
}
case '#documentType': {
const reference = /** @type {DefaultTreeAdapterMap['documentType']} */ (
node
)
result = {type: 'doctype'}
patch(state, reference, result)
return result
}
case '#text': {
const reference = /** @type {DefaultTreeAdapterMap['textNode']} */ (node)
result = {type: 'text', value: reference.value}
patch(state, reference, result)
return result
}
// Element.
default: {
const reference = /** @type {DefaultTreeAdapterMap['element']} */ (node)
result = element(state, reference)
return result
}
}
}
/**
* Transform children.
*
* @param {State} state
* Info passed around about the current state.
* @param {Array<DefaultTreeAdapterMap['node']>} nodes
* Nodes.
* @returns {Array<RootContent>}
* hast nodes.
*/
function all(state, nodes) {
let index = -1
/** @type {Array<RootContent>} */
const results = []
while (++index < nodes.length) {
// Assume no roots in `nodes`.
const result = /** @type {RootContent} */ (one(state, nodes[index]))
results.push(result)
}
return results
}
/**
* Transform an element.
*
* @param {State} state
* Info passed around about the current state.
* @param {DefaultTreeAdapterMap['element']} node
* `parse5` node to transform.
* @returns {Element}
* hast node.
*/
function element(state, node) {
const schema = state.schema
state.schema = node.namespaceURI === webNamespaces.svg ? svg : html
// Props.
let index = -1
/** @type {Record<string, string>} */
const properties = {}
while (++index < node.attrs.length) {
const attribute = node.attrs[index]
const name =
(attribute.prefix ? attribute.prefix + ':' : '') + attribute.name
if (!own.call(proto, name)) {
properties[name] = attribute.value
}
}
// Build.
const x = state.schema.space === 'svg' ? s : h
const result = x(node.tagName, properties, all(state, node.childNodes))
patch(state, node, result)
// Switch content.
if (result.tagName === 'template') {
const reference = /** @type {DefaultTreeAdapterMap['template']} */ (node)
const pos = reference.sourceCodeLocation
const startTag = pos && pos.startTag && position(pos.startTag)
const endTag = pos && pos.endTag && position(pos.endTag)
// Root in, root out.
const content = /** @type {Root} */ (one(state, reference.content))
if (startTag && endTag && state.file) {
content.position = {start: startTag.end, end: endTag.start}
}
result.content = content
}
state.schema = schema
return result
}
/**
* Patch positional info from `from` onto `to`.
*
* @param {State} state
* Info passed around about the current state.
* @param {DefaultTreeAdapterMap['node']} from
* p5 node.
* @param {Nodes} to
* hast node.
* @returns {undefined}
* Nothing.
*/
function patch(state, from, to) {
if ('sourceCodeLocation' in from && from.sourceCodeLocation && state.file) {
const position = createLocation(state, to, from.sourceCodeLocation)
if (position) {
state.location = true
to.position = position
}
}
}
/**
* Create clean positional information.
*
* @param {State} state
* Info passed around about the current state.
* @param {Nodes} node
* hast node.
* @param {Token.ElementLocation} location
* p5 location info.
* @returns {Position | undefined}
* Position, or nothing.
*/
function createLocation(state, node, location) {
const result = position(location)
if (node.type === 'element') {
const tail = node.children[node.children.length - 1]
// Bug for unclosed with children.
// See: <https://github.com/inikulin/parse5/issues/109>.
if (
result &&
!location.endTag &&
tail &&
tail.position &&
tail.position.end
) {
result.end = Object.assign({}, tail.position.end)
}
if (state.verbose) {
/** @type {Record<string, Position | undefined>} */
const properties = {}
/** @type {string} */
let key
if (location.attrs) {
for (key in location.attrs) {
if (own.call(location.attrs, key)) {
properties[find(state.schema, key).property] = position(
location.attrs[key]
)
}
}
}
assert(location.startTag, 'a start tag should exist')
const opening = position(location.startTag)
const closing = location.endTag ? position(location.endTag) : undefined
/** @type {ElementData['position']} */
const data = {opening}
if (closing) data.closing = closing
data.properties = properties
node.data = {position: data}
}
}
return result
}
/**
* Turn a p5 location into a position.
*
* @param {Token.Location} loc
* Location.
* @returns {Position | undefined}
* Position or nothing.
*/
function position(loc) {
const start = point({
line: loc.startLine,
column: loc.startCol,
offset: loc.startOffset
})
const end = point({
line: loc.endLine,
column: loc.endCol,
offset: loc.endOffset
})
// @ts-expect-error: we do use `undefined` for points if one or the other
// exists.
return start || end ? {start, end} : undefined
}
/**
* Filter out invalid points.
*
* @param {Point} point
* Point with potentially `undefined` values.
* @returns {Point | undefined}
* Point or nothing.
*/
function point(point) {
return point.line && point.column ? point : undefined
}