/** Zero dependency web component to show a JavaScript object as a highlighted box in the UI
*
* Version: See the class code
*/
/** Copyright (c) 2022-2025 Julian Knight (Totally Information)
* https://it.knightnet.org.uk, https://github.com/TotallyInformation
*
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/** To Do
* - Use highlighter from visible-console
*/
import TiBaseComponent from '../libs/ti-base-component'
/** Only use a template if you want to isolate the code and CSS */
const template = document.createElement('template')
template.innerHTML = /*html*/`
<style>
:host {
display: block; /* default is inline */
contain: content; /* performance boost */
color:white;
background-color:black;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
padding: 0.5rem;
}
pre {
font-family: Consolas, "ui-monospace", "Lucida Console", monospace;
white-space: pre;
margin: 0;
}
.key {color:#ffbf35}
.string {color:#5dff39;}
.number {color:#70aeff;}
.boolean {color:#b993ff;}
.null {color:#93ffe4;}
.undefined {color:#ff93c9;}
</style>
<slot></slot>
<pre><i>Waiting for data</i></pre>
`
/** Namespace
* @namespace Beta
*/
/**
* @class
* @augments TiBaseComponent
* @description Define a new zero dependency custom web component ECMA module that can be used as an HTML tag
*
* @element syntax-highlight
* @memberOf Beta
* @example Set the json property on the element
* const showMsg = document.getElementsByTagName('syntax-highlight')[0]
* showMsg.json({....})
* METHODS FROM BASE: (see TiBaseComponent)
* STANDARD METHODS:
* @function attributeChangedCallback Called when an attribute is added, removed, updated or replaced
* @function connectedCallback Called when the element is added to a document
* @function constructor Construct the component
* @function disconnectedCallback Called when the element is removed from a document
* OTHER METHODS:
* @function highlight Convert object to HTML
* CUSTOM EVENTS:
* "component-template:connected" - When an instance of the component is attached to the DOM. `evt.details` contains the details of the element.
* "component-template:ready" - Alias for connected. The instance can handle property & attribute changes
* "component-template:disconnected" - When an instance of the component is removed from the DOM. `evt.details` contains the details of the element.
* "component-template:attribChanged" - When a watched attribute changes. `evt.details.data` contains the details of the change.
* NOTE that listeners can be attached either to the `document` or to the specific element instance.
* Standard watched attributes (common across all my components):
* @property {string|boolean} inherit-style - Optional. Load external styles into component (only useful if using template). If present but empty, will default to './index.css'. Optionally give a URL to load.
* @property {string} name - Optional. HTML name attribute. Included in output _meta prop.
* Other watched attributes:
* None
* PROPS FROM BASE: (see TiBaseComponent)
* OTHER STANDARD PROPS:
* @property {string} componentVersion Static. The component version string (date updated). Also has a getter that returns component and base version strings.
* Other props:
* @property {object|JSON|string} json JSON to convert
* By default, all attributes are also created as properties
* @slot Container contents
* See https://github.com/runem/web-component-analyzer?tab=readme-ov-file#-how-to-document-your-components-using-jsdoc
*/
class SyntaxHighlight extends TiBaseComponent {
/** Component version */
static componentVersion = '2025-02-25'
jsonData = {}
/** Makes HTML attribute change watched
* @returns {Array<string>} List of all of the html attribs (props) listened to
*/
static get observedAttributes() {
return [
// Standard watched attributes:
'inherit-style', 'name',
// Other watched attributes:
'auto',
]
}
/** Element.json = {...} to show the json object
* @param {object | JSON | string} value JS Object or JSON string to show
*/
set json(value) {
if (typeof value === 'string') {
try {
value = JSON.parse(value)
} catch (e) {
console.warn(`[syntax-highlight] Could not parse string "${value}"`)
}
}
this.jsonData = value
// console.log(value, syntaxHighlight(value))
this.shadowRoot.removeChild(this.shadowRoot.lastElementChild)
// @ts-ignore
this.shadowRoot.appendChild(this.constructor.highlight(value))
}
get json() {
return this.jsonData
}
/** NB: Attributes not available here - use connectedCallback to reference */
constructor() {
super()
this.attachShadow({ mode: 'open', delegatesFocus: true, })
// Only append the template if code and style isolation is needed
.append(template.content.cloneNode(true))
// jQuery-like selectors but for the shadow. NB: Returns are STATIC not dynamic lists
this.createShadowSelectors() // in base class
}
/** Runs when an instance is added to the DOM */
connectedCallback() {
// Make sure instance has an ID. Create an id from name or calculation if needed
this.ensureId() // in base class
// Apply parent styles from a stylesheet if required - only required if using an applied template
this.doInheritStyles() // in base class
// this.addEventListener('new-msg', evt => {
// this.json = evt.detail
// })
// OPTIONAL. Listen for a uibuilder msg that is targetted at this instance of the component
if (this.uib) document.addEventListener(`uibuilder:msg:_ui:update:${this.id}`, this._uibMsgHandler.bind(this) )
// Keep at end. Let everyone know that a new instance of the component has been connected
this._event('connected')
this._event('ready')
}
/** Runs when an instance is removed from the DOM */
disconnectedCallback() {
// @ts-ignore Remove optional uibuilder event listener
document.removeEventListener(`uibuilder:msg:_ui:update:${this.id}`, this._uibMsgHandler )
// Keep at end. Let everyone know that an instance of the component has been disconnected
this._event('disconnected')
}
/** Runs when an observed attribute changes - Note: values are always strings
* @param {string} attrib Name of watched attribute that has changed
* @param {string} oldVal The previous attribute value
* @param {string} newVal The new attribute value
*/
attributeChangedCallback(attrib, oldVal, newVal) {
// Don't bother if the new value same as old
if ( oldVal === newVal ) return
// Create a property from the value - WARN: Be careful with name clashes
this[attrib] = newVal
// Add other dynamic attribute processing here.
// If attribute processing doesn't need to be dynamic, process in connectedCallback as that happens earlier in the lifecycle
// Keep at end. Let everyone know that an attribute has changed for this instance of the component
this._event('attribChanged', { attribute: attrib, newVal: newVal, oldVal: oldVal, })
} // --- end of attributeChangedCallback --- //
/** Return a formatted HTML version of JSON object
* @param {object|JSON} json JSON object to convert
* @returns {HTMLElement} Highlighted JSON wrapped in a `<pre>` tag
*/
static highlight(json) {
json = JSON.stringify(json, undefined, 4)
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') // eslint-disable-line @stylistic/newline-per-chained-call
json = json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
let cls = 'number'
if ((/^"/).test(match)) {
if ((/:$/).test(match)) {
cls = 'key'
} else {
cls = 'string'
}
} else if ((/true|false/).test(match)) {
cls = 'boolean'
} else if ((/null/).test(match)) {
cls = 'null'
}
return '<span class="' + cls + '">' + match + '</span>'
})
const myHtml = document.createElement('pre')
myHtml.innerHTML = json
return myHtml
}
} // ---- End of SyntaxHighlight class ---- //
// Make the class the default export so it can be used elsewhere
export default SyntaxHighlight
/** Self register the class to global
* Enables new data lists to be dynamically added via JS
* and lets the static methods be called
*/
window['SyntaxHighlight'] = SyntaxHighlight
// Self-register the HTML tag
customElements.define('syntax-highlight', SyntaxHighlight)