Сaмый простой способ - наслоить группу TextArea друг за другом и синхронно фильтровать в них текст соответственно синтаксической подкраске.
Однако, имеются некоторые нюансы.
<html>
<head>
<title>Подсветка синтаксиса</title>
<script>
class FineTextArea extends HTMLTextAreaElement {
constructor() {
super();
}
connectedCallback() {
var update = this.update.bind(this, window.event, true);
this.buddy = document.createElement("TextArea");
this.buddy.className = "FineTextArea-Buddy";
this.buddy.id = this.id + "_buddy";
this.buddy.disabled = true;
this.parentElement.insertBefore(this.buddy, this);
this.addEventListener("change", this.update.bind(this));
this.addEventListener("keyup", this.update.bind(this));
this.addEventListener("keydown", this.update.bind(this));
this.addEventListener("keypress", this.update.bind(this));
this.addEventListener("mousemove", this.update.bind(this));
this.addEventListener("mouseup", this.update.bind(this));
this.addEventListener("scroll", this.update.bind(this));
// Создаём элементы подсветки синтаксиса
*!*
const keywords = "dcr hlt inr inx lxi mov sphl xra jp".replace(/\s+/g, "|");
this.syntaxers = [];
this.syntexps = [
new RegExp(`\\b(${keywords})\\b`, "gim"),
new RegExp(`\\b(?!${keywords})\\b`, "gim"),
new RegExp(`\\b(${keywords})\\b`, "gim"),
/[^\x21-\x7F\n\r\s\t]+/gim,
/[\x21-\x7F]+/gim
];
*/!*
for(var i in this.syntexps) {
var syn = document.createElement("TextArea");
syn.className = "FineTextArea-Syntax" + i;
syn.id = this.id + "_syntax" + i;
syn.disabled = true;
this.parentElement.insertBefore(syn, this);
this.syntaxers.push(syn);
}
// Форсируем (альфа-отладка)
this.lines = true;
this.syntax = true;
setTimeout(update, 0); setTimeout(update, 0);
}
set lines(value) {
this._lines = value;
this.buddy.style.visibility = value != false ? "visible" : "hidden";
this.style.paddingLeft = value != false ? this.buddy.offsetWidth + "px" : "0px";
if(value != false)
this.update(window.event, true);
}
set syntax(value) {
this._syntax = !!value;
this.update(window.event, true);
}
get row() {
return this.value.substr(0, this.selectionStart).split(/\r?\n/).length;
}
get col() {
return this.value.substr(0, this.selectionEnd).split(/\r?\n/).pop().length + 1;
}
update(evt, force) {
var buttons = evt && evt.buttons || 0;
var rows = this.value.split(/\r?\n/), len = rows.length.toString().length;
var syn, i, row = 1;
this.buddy.scrollTop = this.scrollTop;
for(syn of this.syntaxers)
syn.scrollTop = this.scrollTop;
var spc = ("string" == typeof this._lines && this._lines != "" ? this._lines.charAt(0) : "\xA0").repeat(len);
var rex = new RegExp(`([ -])([^ -]{${len}})`, "gim"); // Выявление первых символов каждого слова
// Распределяем текст по слоям
var value = this.value, box = "\u2588";
for(i in this.syntexps) {
this.syntaxers[i].value = value.replace(this.syntexps[i], s => box.repeat(s.length));
box = "\xA0";
if(i != 0)
value = value.replace(this.syntexps[i], s => box.repeat(s.length));
}
if((buttons == 0 && (Math.abs(this.buddy.scrollHeight - this.scrollHeight) > 30 || this.buddy.scrollWidth != this.scrollWidth)) || force == true) {
var width;
var height;
var bg;
// Здесь следуют некоторые манипуляции со стилями
this.style.width = (parseInt(window.getComputedStyle(this, null).getPropertyValue("width")) & 0xFFE) + "px";
this.buddy.style.paddingRight = "0px";
this.buddy.style.width = "auto";
this.buddy.cols = len;
// Маскируем задний план так, кроме области нумерации
bg = `linear-gradient(90deg, rgba(0,0,0,0) ${this.buddy.clientWidth - 1}px, white ${this.buddy.clientWidth}px, white 100%)`;
if(this._syntax == false || buttons)
this.style.background = bg;
else
this.style.background = "transparent",
this.syntaxers[0].style.background = bg;
this.style.paddingLeft = `${this.buddy.clientWidth}px`;
// Расчёт
height = window.getComputedStyle(this, null).getPropertyValue("height");
width = window.getComputedStyle(this, null).getPropertyValue("width");
// Обновляем синтаксические помощники
for(syn of this.syntaxers) {
syn.cols = this.cols;
syn.style.paddingLeft = `${this.buddy.clientWidth}px`;
syn.style.height = height;
syn.style.width = width;
}
// Копируем визуальные реквизиты элемента
this.buddy.cols = this.cols;
this.buddy.style.height = height;
this.buddy.style.width = width;
// Компенсируем визуальные отступы у текста и нумератора
this.buddy.style.paddingLeft = window.getComputedStyle(this, null).getPropertyValue("padding-right");
this.buddy.style.paddingRight = window.getComputedStyle(this, null).getPropertyValue("padding-left");
if(this.scrollHeight != this.buddy.scrollHeight)
force = !true;
}
// Затем всё совсем просто
if(force == true || this._value != this.value) {
// Переносим весь текст в поле нумератора, кроме первых символов каждого слова, заменяя их нумерацией
console.time("Numbering");
for(var fine = 0; fine < 2; ++ fine) {
this.buddy.value = this.value.replace(/^.*$/gm, function(str) {
return Number(row ++).toString().padStart(len, spc) + str.substr(str.charAt(0) == "\t" ? 0 : len).replace(rex, `$1${spc}`);
}
);
if(this.scrollHeight == this.buddy.scrollHeight)
fine = 1;
else
fine = 1;
}
console.timeEnd("Numbering");
this._value = this.value;
}
this.buddy.style.visibility = buttons || this._lines == false ? "hidden" : "visible";
this.buddy.scrollTop = this.scrollTop;
for(syn of this.syntaxers)
syn.scrollTop = this.scrollTop,
syn.style.visibility = buttons || this._syntax == false ? "hidden" : "visible";
this.style.color = buttons || this._syntax == false ? "black" : "transparent";
}
static get observedAttributes() {
return "lines syntax".split(/\s+/);
}
attributeChangedCallback(name, oldValue, newValue) {
switch(name) {
case "lines":
this._lines = newValue;
break;
case "syntax":
this._syntax = !!newValue;
//this.update(window.event, true);
break;
}
}
}
customElements.define("fine-textarea", FineTextArea, {extends: "textarea"});
</script>
<style>
textarea[is='fine-textarea']
{
position :relative;
background-color:transparent;
color :transparent;
caret-color :black;
}
textarea.FineTextArea-Buddy
{
position :absolute;
background-color:yellow;
}
textarea.FineTextArea-Syntax0,
textarea.FineTextArea-Syntax1,
textarea.FineTextArea-Syntax2,
textarea.FineTextArea-Syntax3,
textarea.FineTextArea-Syntax4,
textarea.FineTextArea-Syntax5,
textarea.FineTextArea-Syntax6,
textarea.FineTextArea-Syntax7,
textarea.FineTextArea-Syntax8,
textarea.FineTextArea-Syntax9
{
position :absolute;
background-color:transparent;
color :orange;
animation-duration: 10s;
animation-timing-function: linear;
animation-name :Layer;
animation-iteration-count: infinite;
}
textarea.FineTextArea-Syntax0 { color :lightgreen; --xy:60px;}
textarea.FineTextArea-Syntax1 { color :darkgreen; --xy:110px;}
textarea.FineTextArea-Syntax2 { color :magenta; --xy:160px;}
textarea.FineTextArea-Syntax3 { color :blue; --xy:210px;}
@keyframes Layer {
from, 33% {
transform:translate(0px, 0px);
}
50%,100% {
transform:translate(var(--xy), var(--xy));
}
}
</style>
</head>
<body>
<input type=text placeholder='Атрибут = Значение' onchange='this.style.backgroundColor = "red"; eval(`hTextArea.${this.value}`); this.style.backgroundColor = "yellow"'>
<input type=text placeholder='Цвет фона нумератора' onchange='hTextArea.buddy.style.backgroundColor = this.value'><br>
<textarea rows=15 cols=40 id=Main spellcheck=false title='Текстовое поле для редактирования' is=fine-textarea lines=true syntax=true>
____________________________________
|Проверка цветовой подсветки V0.003|
====================================
lxi h,076D0h
sphl
xra a
L1: mov m,a
inx h
dcr h
inr h
jp L1
hlt
</textarea>
<script>
var hTextArea = document.querySelector("textarea#Main");
</script>
</body>
Как видно из анимации в примере, всё работает до примитивного просто в пользовательском элементе.
Но, есть проблемы в строках #43-45 (без этого сбивается изначальный вид) и в строках #25-33 (нужно каким-то атрибутом унифицировать управление правилами синтаксической фильтрации).
P.S.: Данная тема - логическое развитие
предыдущей…