752 lines
22 KiB
JavaScript
752 lines
22 KiB
JavaScript
/* global CodeMirror */
|
|
/* global colorConverter */
|
|
'use strict';
|
|
|
|
(() => {
|
|
//region Constants
|
|
|
|
const COLORVIEW_CLASS = 'colorview';
|
|
const COLORVIEW_SWATCH_CLASS = COLORVIEW_CLASS + '-swatch';
|
|
const COLORVIEW_SWATCH_CSS = `--${COLORVIEW_SWATCH_CLASS}:`;
|
|
const CLOSE_POPUP_EVENT = 'close-colorpicker-popup';
|
|
|
|
const {RX_COLOR, testAt} = colorConverter;
|
|
const RX_UNSUPPORTED = (s => s && new RegExp(s))([
|
|
!CSS.supports('color', '#abcd') && /#(.{4}){1,2}$/,
|
|
!CSS.supports('color', 'hwb(1 0% 0%)') && /^hwb\(/,
|
|
!CSS.supports('color', 'rgb(1e2,0,0)') && /\de/,
|
|
!CSS.supports('color', 'rgb(1.5,0,0)') &&
|
|
/^rgba?\((([^,]+,){0,2}[^,]*\.|(\s*\S+\s+){0,2}\S*\.)/,
|
|
!CSS.supports('color', 'rgb(1,2,3,.5)') && /[^a]\(([^,]+,){3}/,
|
|
!CSS.supports('color', 'rgb(1,2,3,50%)') && /\((([^,]+,){3}|(\s*\S+[\s/]+){3}).*?%/,
|
|
!CSS.supports('color', 'rgb(1 2 3 / 1)') && /^[^,]+$/,
|
|
!CSS.supports('color', 'hsl(1turn, 2%, 3%)') && /deg|g?rad|turn/,
|
|
].filter(Boolean).map(rx => rx.source).join('|'));
|
|
const RX_DETECT = new RegExp('(^|[\\s(){}[\\]:,/"=])' +
|
|
'(' +
|
|
RX_COLOR.hex.source + '|' +
|
|
'(?:(?:rgb|hsl)a?|hwb)(?=\\()|(?:' + [...colorConverter.NAMED_COLORS.keys()].join('|') + ')' +
|
|
'(?=[\\s;(){}[\\]/"!]|$)' +
|
|
')', 'gi');
|
|
const RX_DETECT_FUNC = /((rgb|hsl)a?|hwb)\(/iy;
|
|
const RX_COMMENT = /\/\*([^*]+|\*(?!\/))*(\*\/|$)/g;
|
|
const SPACE1K = ' '.repeat(1000);
|
|
|
|
// milliseconds to work on invisible colors per one run
|
|
const TIME_BUDGET = 50;
|
|
|
|
// on initial paint the view doesn't have a size yet
|
|
// so we process the maximum number of lines that can fit in the window
|
|
let maxRenderChunkSize = Math.ceil(window.innerHeight / 14);
|
|
|
|
//endregion
|
|
//region CodeMirror Events
|
|
|
|
const CM_EVENTS = {
|
|
changes(cm, info) {
|
|
colorizeChanges(cm.state.colorpicker, info);
|
|
},
|
|
update(cm) {
|
|
const textHeight = cm.display.cachedTextHeight;
|
|
const height = cm.display.lastWrapHeight;
|
|
if (!height || !textHeight) return;
|
|
maxRenderChunkSize = Math.max(20, Math.ceil(height / textHeight));
|
|
const state = cm.state.colorpicker;
|
|
if (state.colorizeOnUpdate) {
|
|
state.colorizeOnUpdate = false;
|
|
colorizeAll(state);
|
|
}
|
|
cm.off('update', CM_EVENTS.update);
|
|
},
|
|
mousedown(cm, event) {
|
|
const state = cm.state.colorpicker;
|
|
const swatch = hitTest(event);
|
|
dispatchEvent(new CustomEvent(CLOSE_POPUP_EVENT, {
|
|
detail: swatch && state.popup,
|
|
}));
|
|
if (swatch) {
|
|
event.preventDefault();
|
|
openPopupForSwatch(state, swatch);
|
|
}
|
|
},
|
|
};
|
|
|
|
//endregion
|
|
//region ColorSwatch
|
|
|
|
const cache = new Set();
|
|
|
|
class ColorSwatch {
|
|
constructor(cm, options = {}) {
|
|
this.cm = cm;
|
|
this.options = options;
|
|
this.markersToRemove = [];
|
|
this.markersToRepaint = [];
|
|
this.popup = cm.colorpicker && cm.colorpicker();
|
|
if (!this.popup) {
|
|
delete CM_EVENTS.mousedown;
|
|
document.head.appendChild(document.createElement('style')).textContent = `
|
|
.colorview-swatch::before {
|
|
cursor: auto;
|
|
}
|
|
`;
|
|
}
|
|
this.colorize();
|
|
this.registerEvents();
|
|
}
|
|
|
|
colorize() {
|
|
colorizeAll(this);
|
|
}
|
|
|
|
openPopup(color) {
|
|
if (this.popup) openPopupForCursor(this, color);
|
|
}
|
|
|
|
registerEvents() {
|
|
for (const name in CM_EVENTS) {
|
|
this.cm.on(name, CM_EVENTS[name]);
|
|
}
|
|
}
|
|
|
|
unregisterEvents() {
|
|
for (const name in CM_EVENTS) {
|
|
this.cm.off(name, CM_EVENTS[name]);
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this.unregisterEvents();
|
|
const {cm} = this;
|
|
const {curOp} = cm;
|
|
if (!curOp) cm.startOperation();
|
|
cm.getAllMarks().forEach(m => m.className === COLORVIEW_CLASS && m.clear());
|
|
if (!curOp) cm.endOperation();
|
|
cm.state.colorpicker = null;
|
|
}
|
|
}
|
|
|
|
//endregion
|
|
//region CodeMirror registration
|
|
|
|
CodeMirror.defineOption('colorpicker', false, (cm, value, oldValue) => {
|
|
if (oldValue && oldValue !== CodeMirror.Init && cm.state.colorpicker) {
|
|
cm.state.colorpicker.destroy();
|
|
}
|
|
if (value) {
|
|
cm.state.colorpicker = new ColorSwatch(cm, value);
|
|
}
|
|
});
|
|
|
|
CodeMirror.prototype.getStyleAtPos = getStyleAtPos;
|
|
|
|
return;
|
|
|
|
//endregion
|
|
//region Colorizing
|
|
|
|
function colorizeAll(state) {
|
|
const {cm} = state;
|
|
const {viewFrom, viewTo} = cm.display;
|
|
if (!viewTo) {
|
|
state.colorizeOnUpdate = true;
|
|
return;
|
|
}
|
|
const {curOp} = cm;
|
|
if (!curOp) cm.startOperation();
|
|
|
|
state.line = viewFrom;
|
|
state.inComment = null;
|
|
state.now = performance.now();
|
|
state.stopAt = state.stopped = null;
|
|
|
|
cm.doc.iter(viewFrom, viewTo, lineHandle => colorizeLine(state, lineHandle));
|
|
|
|
updateMarkers(state);
|
|
if (!curOp) cm.endOperation();
|
|
|
|
if (viewFrom > 0 || viewTo < cm.doc.size) {
|
|
clearTimeout(state.colorizeTimer);
|
|
state.line = 0;
|
|
state.colorizeTimer = setTimeout(colorizeInvisible, 100, state, viewFrom, viewTo);
|
|
}
|
|
}
|
|
|
|
|
|
function colorizeInvisible(state, viewFrom, viewTo) {
|
|
const {cm} = state;
|
|
const {curOp} = cm;
|
|
if (!curOp) cm.startOperation();
|
|
|
|
state.now = performance.now();
|
|
state.stopAt = state.now + TIME_BUDGET;
|
|
state.stopped = null;
|
|
|
|
// before the visible range
|
|
cm.eachLine(state.line, viewFrom, lineHandle => colorizeLine(state, lineHandle));
|
|
|
|
// after the visible range
|
|
if (!state.stopped && viewTo < cm.doc.size) {
|
|
state.line = Math.max(viewTo, state.line);
|
|
cm.eachLine(state.line, cm.doc.size, lineHandle => colorizeLine(state, lineHandle));
|
|
}
|
|
|
|
updateMarkers(state);
|
|
if (!curOp) cm.endOperation();
|
|
|
|
if (state.stopped) {
|
|
state.colorizeTimer = setTimeout(colorizeInvisible, 0, state, viewFrom, viewTo);
|
|
}
|
|
}
|
|
|
|
|
|
function colorizeChanges(state, changes) {
|
|
if (changes.length === 1 && changes[0].origin === 'setValue') {
|
|
colorizeAll(state);
|
|
return;
|
|
}
|
|
const queue = [];
|
|
const postponed = [];
|
|
const viewFrom = state.cm.display.viewFrom || 0;
|
|
const viewTo = state.cm.display.viewTo || viewFrom + maxRenderChunkSize;
|
|
|
|
for (let change of changes) {
|
|
const {from} = change;
|
|
const to = CodeMirror.changeEnd(change);
|
|
const offscreen = from.line > viewTo || to.line < viewFrom;
|
|
if (offscreen) {
|
|
postponed.push(change);
|
|
continue;
|
|
}
|
|
if (from.line < viewFrom) {
|
|
postponed.push(Object.assign({}, change, {to: {line: viewFrom - 1}}));
|
|
change = Object.assign({}, change, {from: {line: viewFrom}});
|
|
}
|
|
if (to.line > viewTo) {
|
|
postponed.push(Object.assign({}, change, {from: {line: viewTo + 1}}));
|
|
change = Object.assign({}, change, {to: {line: viewTo}});
|
|
}
|
|
queue.push(change);
|
|
}
|
|
|
|
if (queue.length) colorizeChangesNow(state, queue);
|
|
if (postponed.length) setTimeout(colorizeChangesNow, 0, state, postponed, true);
|
|
}
|
|
|
|
|
|
function colorizeChangesNow(state, changes, canPostpone) {
|
|
const {cm} = state;
|
|
const {curOp} = cm;
|
|
if (!curOp) cm.startOperation();
|
|
|
|
state.now = performance.now();
|
|
const stopAt = canPostpone && state.now + TIME_BUDGET;
|
|
let stopped = null;
|
|
|
|
let change, changeFromLine;
|
|
let changeToLine = -1;
|
|
let queueIndex = -1;
|
|
|
|
changes = changes.sort((a, b) => a.from.line - b.from.line || a.from.ch - b.from.ch);
|
|
const first = changes[0].from.line;
|
|
const last = CodeMirror.changeEnd(changes[changes.length - 1]).line;
|
|
let line = state.line = first;
|
|
|
|
cm.doc.iter(first, last + 1, lineHandle => {
|
|
if (line > changeToLine) {
|
|
change = changes[++queueIndex];
|
|
if (!change) return true;
|
|
changeFromLine = change.from.line;
|
|
changeToLine = CodeMirror.changeEnd(change).line;
|
|
}
|
|
if (changeFromLine <= line && line <= changeToLine) {
|
|
state.line = line;
|
|
if (!lineHandle.styles) state.cm.getTokenTypeAt({line, ch: 0});
|
|
colorizeLineViaStyles(state, lineHandle);
|
|
}
|
|
if (canPostpone && (state.now = performance.now()) > stopAt) {
|
|
stopped = true;
|
|
return true;
|
|
}
|
|
line++;
|
|
});
|
|
|
|
updateMarkers(state);
|
|
if (!curOp) cm.endOperation();
|
|
|
|
if (stopped) {
|
|
const stoppedInChange = line >= changeFromLine && line < changeToLine;
|
|
if (stoppedInChange) {
|
|
changes.splice(0, queueIndex);
|
|
changes[0] = Object.assign({}, changes[0], {from: {line}});
|
|
} else {
|
|
changes.splice(0, queueIndex + 1);
|
|
}
|
|
state.colorizeTimer = setTimeout(colorizeChangesNow, 0, state, changes, true);
|
|
}
|
|
}
|
|
|
|
|
|
function colorizeLine(state, lineHandle) {
|
|
if (state.stopAt && (state.now = performance.now()) > state.stopAt) {
|
|
state.stopped = true;
|
|
return true;
|
|
}
|
|
const {text, styles} = lineHandle;
|
|
const {cm} = state;
|
|
|
|
if (state.inComment === null && !styles) {
|
|
cm.getTokenTypeAt({line: state.line, ch: 0});
|
|
colorizeLineViaStyles(state, lineHandle);
|
|
return;
|
|
}
|
|
|
|
if (styles) {
|
|
colorizeLineViaStyles(state, lineHandle);
|
|
return;
|
|
}
|
|
|
|
let cmtStart = 0;
|
|
let cmtEnd = 0;
|
|
do {
|
|
if (state.inComment) {
|
|
cmtEnd = text.indexOf('*/', cmtStart);
|
|
if (cmtEnd < 0) break;
|
|
state.inComment = false;
|
|
cmtEnd += 2;
|
|
}
|
|
cmtStart = (text.indexOf('/*', cmtEnd) + 1 || text.length + 1) - 1;
|
|
const chunk = !cmtEnd && cmtStart === text.length ? text : text.slice(cmtEnd, cmtStart);
|
|
|
|
RX_DETECT.lastIndex = 0;
|
|
const m = RX_DETECT.exec(chunk);
|
|
if (m) {
|
|
cmtEnd += m.index + m[1].length;
|
|
cm.getTokenTypeAt({line: state.line, ch: 0});
|
|
const {index} = getStyleAtPos({styles: lineHandle.styles, pos: cmtEnd}) || {};
|
|
colorizeLineViaStyles(state, lineHandle, Math.max(1, index || 0));
|
|
return;
|
|
}
|
|
state.inComment = cmtStart < text.length;
|
|
} while (state.inComment);
|
|
state.line++;
|
|
}
|
|
|
|
|
|
function colorizeLineViaStyles(state, lineHandle, styleIndex = 1) {
|
|
const {styles} = lineHandle;
|
|
let {text} = lineHandle;
|
|
let spanIndex = 0;
|
|
let uncommented = false;
|
|
let span, style, start, end, len, isHex, isFunc, color;
|
|
|
|
let {markedSpans} = lineHandle;
|
|
let spansSorted = false;
|
|
let spansZombies = markedSpans && markedSpans.length;
|
|
const spanGeneration = state.now;
|
|
|
|
// all comments may get blanked out in the loop
|
|
const endsWithComment = text.endsWith('*/');
|
|
|
|
for (let i = styleIndex; i + 1 < styles.length; i += 2) {
|
|
style = styles[i + 1];
|
|
const styleSupported = style && (
|
|
// old CodeMirror
|
|
style.includes('atom') || style.includes('keyword') ||
|
|
// new CodeMirror since 5.48
|
|
style.includes('variable callee')
|
|
);
|
|
if (!styleSupported) continue;
|
|
|
|
start = i > 2 ? styles[i - 2] : 0;
|
|
end = styles[i];
|
|
len = end - start;
|
|
isHex = text[start] === '#';
|
|
isFunc = text[end] === '(';
|
|
|
|
if (isFunc && (len < 3 || len > 4 || !testAt(RX_DETECT_FUNC, start, text))) continue;
|
|
if (isFunc && !uncommented) {
|
|
text = blankOutComments(text, start);
|
|
uncommented = true;
|
|
}
|
|
|
|
color = text.slice(start, isFunc ? text.indexOf(')', end) + 1 : end);
|
|
const j = !isHex && !isFunc && color.indexOf('!');
|
|
if (j > 0) {
|
|
color = color.slice(0, j);
|
|
end = start + j;
|
|
}
|
|
const spanState = markedSpans && checkSpan();
|
|
if (spanState === 'same') continue;
|
|
if (checkColor()) {
|
|
(spanState ? redeem : mark)(getSafeColorValue());
|
|
}
|
|
}
|
|
|
|
removeDeadSpans();
|
|
|
|
state.inComment = style && style.includes('comment') && !endsWithComment;
|
|
state.line++;
|
|
return;
|
|
|
|
function checkColor() {
|
|
if (isHex) return testAt(RX_COLOR.hex, 0, color);
|
|
if (!isFunc) return colorConverter.NAMED_COLORS.has(color.toLowerCase());
|
|
|
|
const colorLower = color.toLowerCase();
|
|
if (cache.has(colorLower)) return true;
|
|
|
|
const type = color.substr(0, 3);
|
|
const value = color.slice(len + 1, -1);
|
|
if (!testAt(RX_COLOR[type], 0, value)) return false;
|
|
|
|
cache.add(colorLower);
|
|
return true;
|
|
}
|
|
|
|
function mark(colorValue) {
|
|
const {line} = state;
|
|
state.cm.markText({line, ch: start}, {line, ch: end}, {
|
|
className: COLORVIEW_CLASS,
|
|
startStyle: COLORVIEW_SWATCH_CLASS,
|
|
css: COLORVIEW_SWATCH_CSS + colorValue,
|
|
color,
|
|
});
|
|
}
|
|
|
|
function getSafeColorValue() {
|
|
if (isHex && color.length !== 5 && color.length !== 9) return color;
|
|
if (!RX_UNSUPPORTED || !RX_UNSUPPORTED.test(color)) return color;
|
|
const value = colorConverter.parse(color);
|
|
return colorConverter.format(value, 'rgb');
|
|
}
|
|
|
|
// update or skip or delete existing swatches
|
|
function checkSpan() {
|
|
if (!spansSorted) {
|
|
markedSpans = markedSpans.sort((a, b) => a.from - b.from);
|
|
spansSorted = true;
|
|
}
|
|
while (spanIndex < markedSpans.length) {
|
|
span = markedSpans[spanIndex];
|
|
if (span.from <= start) {
|
|
spanIndex++;
|
|
} else {
|
|
break;
|
|
}
|
|
if (span.from === start && span.marker.className === COLORVIEW_CLASS) {
|
|
spansZombies--;
|
|
span.generation = spanGeneration;
|
|
const same = color === span.marker.color &&
|
|
(isFunc || /\W|^$/i.test(text.substr(start + color.length, 1)));
|
|
if (same) return 'same';
|
|
state.markersToRemove.push(span.marker);
|
|
return 'redeem';
|
|
}
|
|
}
|
|
}
|
|
|
|
function redeem(colorValue) {
|
|
spansZombies++;
|
|
state.markersToRemove.pop();
|
|
state.markersToRepaint.push(span);
|
|
span.to = end;
|
|
span.line = state.line;
|
|
span.index = spanIndex - 1;
|
|
span.marker.color = color;
|
|
span.marker.css = COLORVIEW_SWATCH_CSS + colorValue;
|
|
}
|
|
|
|
function removeDeadSpans() {
|
|
if (!spansZombies) return;
|
|
for (const span of markedSpans) {
|
|
if (span.generation !== spanGeneration &&
|
|
span.marker.className === COLORVIEW_CLASS) {
|
|
state.markersToRemove.push(span.marker);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//endregion
|
|
//region Popup
|
|
|
|
function openPopupForCursor(state, defaultColor) {
|
|
const {line, ch} = state.cm.getCursor();
|
|
const lineHandle = state.cm.getLineHandle(line);
|
|
const data = {
|
|
line, ch,
|
|
color: defaultColor,
|
|
isShortCut: true,
|
|
};
|
|
|
|
let found;
|
|
for (const {from, marker} of lineHandle.markedSpans || []) {
|
|
if (marker.className === COLORVIEW_CLASS &&
|
|
from <= ch && ch < from + marker.color.length) {
|
|
found = {color: marker.color, ch: from};
|
|
break;
|
|
}
|
|
}
|
|
found = found || findNearestColor(lineHandle, ch);
|
|
doOpenPopup(state, Object.assign(data, found));
|
|
if (found) highlightColor(state, data);
|
|
}
|
|
|
|
|
|
function openPopupForSwatch(state, swatch) {
|
|
const cm = state.cm;
|
|
const lineDiv = swatch.closest('div');
|
|
const {line: {markedSpans} = {}} = cm.display.renderedView.find(v => v.node === lineDiv) || {};
|
|
if (!markedSpans) return;
|
|
|
|
let swatchIndex = [...lineDiv.getElementsByClassName(COLORVIEW_SWATCH_CLASS)].indexOf(swatch);
|
|
for (const {marker} of markedSpans.sort((a, b) => a.from - b.from)) {
|
|
if (marker.className === COLORVIEW_CLASS && swatchIndex-- === 0) {
|
|
const data = Object.assign({color: marker.color}, marker.find().from);
|
|
highlightColor(state, data);
|
|
doOpenPopup(state, data);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function doOpenPopup(state, data) {
|
|
const {left, bottom: top} = state.cm.charCoords(data, 'window');
|
|
state.popup.show(Object.assign(state.options.popup, data, {
|
|
top,
|
|
left,
|
|
cm: state.cm,
|
|
color: data.color,
|
|
prevColor: data.color || '',
|
|
callback: popupOnChange,
|
|
palette: makePalette(state),
|
|
paletteCallback,
|
|
}));
|
|
}
|
|
|
|
|
|
function popupOnChange(newColor) {
|
|
if (!newColor) {
|
|
return;
|
|
}
|
|
const {cm, line, ch, embedderCallback} = this;
|
|
const to = {line, ch: ch + this.prevColor.length};
|
|
const from = {line, ch};
|
|
if (cm.getRange(from, to) !== newColor) {
|
|
cm.replaceRange(newColor, from, to, '*colorpicker');
|
|
this.prevColor = newColor;
|
|
}
|
|
if (typeof embedderCallback === 'function') {
|
|
embedderCallback(this);
|
|
}
|
|
}
|
|
|
|
function makePalette({cm, options}) {
|
|
const palette = new Map();
|
|
let i = 0;
|
|
let nums;
|
|
cm.eachLine(({markedSpans}) => {
|
|
++i;
|
|
if (!markedSpans) return;
|
|
for (const {from, marker: m} of markedSpans) {
|
|
if (from == null || m.className !== COLORVIEW_CLASS) continue;
|
|
const color = m.color.toLowerCase();
|
|
nums = palette.get(color);
|
|
if (!nums) palette.set(color, (nums = []));
|
|
nums.push(i);
|
|
}
|
|
});
|
|
const res = [];
|
|
if (palette.size > 1 || nums && nums.length > 1) {
|
|
const old = new Map((options.popup.palette || []).map(el => [el.__color, el]));
|
|
for (const [color, data] of palette) {
|
|
const str = data.join(', ');
|
|
let el = old.get(color);
|
|
if (!el) {
|
|
el = document.createElement('div');
|
|
el.__color = color; // also used in color-picker.js
|
|
el.className = COLORVIEW_SWATCH_CLASS;
|
|
el.style.setProperty(`--${COLORVIEW_SWATCH_CLASS}`, color);
|
|
}
|
|
if (el.__str !== str) {
|
|
el.__str = str;
|
|
// break down long lists: 10 per line
|
|
el.title = `${color}\n${options.popup.paletteLine} ${
|
|
str.length > 50 ? str.replace(/([^,]+,\s){10}/g, '$&\n') : str
|
|
}`;
|
|
}
|
|
res.push(el);
|
|
}
|
|
res.push(Object.assign(document.createElement('span'), {
|
|
className: 'colorpicker-palette-hint',
|
|
title: options.popup.paletteHint,
|
|
textContent: '?',
|
|
}));
|
|
}
|
|
return res;
|
|
}
|
|
|
|
function paletteCallback(el) {
|
|
const {cm} = this;
|
|
const lines = el.title.split('\n')[1].match(/\d+/g).map(Number);
|
|
const i = lines.indexOf(cm.getCursor().line + 1) + 1;
|
|
const line = (lines[i] || lines[0]) - 1;
|
|
cm.jumpToPos({line, ch: 0});
|
|
}
|
|
|
|
//endregion
|
|
//region Utility
|
|
|
|
function updateMarkers(state) {
|
|
state.markersToRemove.forEach(m => m.clear());
|
|
state.markersToRemove.length = 0;
|
|
|
|
const {cm: {display: {viewFrom, viewTo, view}}} = state;
|
|
let viewIndex = 0;
|
|
let lineView = view[0];
|
|
let lineViewLine = viewFrom;
|
|
for (const {line, index, marker} of state.markersToRepaint) {
|
|
if (line < viewFrom || line >= viewTo) continue;
|
|
while (lineViewLine < line && lineView) {
|
|
lineViewLine += lineView.size;
|
|
lineView = view[++viewIndex];
|
|
}
|
|
if (!lineView) break;
|
|
const el = lineView.text.getElementsByClassName(COLORVIEW_SWATCH_CLASS)[index];
|
|
if (el) el.style = marker.css;
|
|
}
|
|
state.markersToRepaint.length = 0;
|
|
}
|
|
|
|
|
|
function findNearestColor({styles, text}, pos) {
|
|
const ALLOWED_STYLES = ['atom', 'keyword', 'callee', 'comment', 'string'];
|
|
let start, color, prevStart, prevColor, m;
|
|
RX_DETECT.lastIndex = Math.max(0, pos - 1000);
|
|
|
|
while ((m = RX_DETECT.exec(text))) {
|
|
start = m.index + m[1].length;
|
|
color = getColor(m[2].toLowerCase());
|
|
if (!color) continue;
|
|
if (start >= pos) break;
|
|
prevStart = start;
|
|
prevColor = color;
|
|
}
|
|
|
|
if (prevColor && pos - (prevStart + prevColor.length) < start - pos) {
|
|
return {color: prevColor, ch: prevStart};
|
|
} else if (color) {
|
|
return {color, ch: start};
|
|
}
|
|
|
|
function getColor(token) {
|
|
const {style} = getStyleAtPos({styles, pos: start + 1}) || {};
|
|
const allowed = !style || ALLOWED_STYLES.includes(style.split(' ', 1)[0]);
|
|
if (!allowed) return;
|
|
|
|
if (text[start + token.length] === '(') {
|
|
const tail = blankOutComments(text.slice(start), 0);
|
|
const color = tail.slice(0, tail.indexOf(')') + 1);
|
|
const type = color.slice(0, 3);
|
|
const value = color.slice(token.length + 1, -1);
|
|
return testAt(RX_COLOR[type], 0, value) && color;
|
|
}
|
|
return (token[0] === '#' || colorConverter.NAMED_COLORS.has(token)) && token;
|
|
}
|
|
}
|
|
|
|
|
|
function highlightColor(state, data) {
|
|
const {line} = data;
|
|
const {cm} = state;
|
|
const {viewFrom, viewTo} = cm.display;
|
|
if (line < viewFrom || line > viewTo) {
|
|
return;
|
|
}
|
|
const first = cm.charCoords(data, 'window');
|
|
const colorEnd = data.ch + data.color.length - 1;
|
|
let last = cm.charCoords({line, ch: colorEnd}, 'window');
|
|
if (last.top !== first.top) {
|
|
const funcEnd = data.ch + data.color.indexOf('(') - 1;
|
|
last = cm.charCoords({line, ch: funcEnd}, 'window');
|
|
}
|
|
const el = document.createElement('div');
|
|
const DURATION_SEC = 1;
|
|
el.style = `
|
|
position: fixed;
|
|
display: block;
|
|
top: ${first.top}px;
|
|
left: ${first.left}px;
|
|
width: ${last.right - first.left}px;
|
|
height: ${last.bottom - first.top}px;
|
|
animation: highlight ${DURATION_SEC}s;
|
|
`;
|
|
document.body.appendChild(el);
|
|
setTimeout(() => el.remove(), DURATION_SEC * 1000);
|
|
}
|
|
|
|
function getStyleAtPos({
|
|
line,
|
|
styles = this.getLineHandle(line).styles,
|
|
pos,
|
|
}) {
|
|
if (pos < 0 || !styles) return;
|
|
const len = styles.length;
|
|
const end = styles[len - 2];
|
|
if (pos > end) return;
|
|
if (pos === end) {
|
|
return {
|
|
style: styles[len - 1],
|
|
index: len - 2,
|
|
};
|
|
}
|
|
const mid = (pos / end * (len - 1) & ~1) + 1;
|
|
let a = mid;
|
|
let b;
|
|
while (a > 1 && styles[a] > pos) {
|
|
b = a;
|
|
a = (a / 2 & ~1) + 1;
|
|
}
|
|
if (!b) b = mid;
|
|
while (b < len && styles[b] < pos) b = ((len + b) / 2 & ~1) + 1;
|
|
while (a < b - 3) {
|
|
const c = ((a + b) / 2 & ~1) + 1;
|
|
if (styles[c] > pos) b = c; else a = c;
|
|
}
|
|
while (a < len && styles[a] < pos) a += 2;
|
|
return {
|
|
style: styles[a + 1],
|
|
index: a,
|
|
};
|
|
}
|
|
|
|
|
|
function blankOutComments(text, start) {
|
|
const cmtStart = text.indexOf('/*', start);
|
|
return cmtStart < 0 ? text : (
|
|
text.slice(0, cmtStart) +
|
|
text.slice(cmtStart)
|
|
.replace(RX_COMMENT, s =>
|
|
SPACE1K.repeat(s.length / 1000 | 0) + SPACE1K.slice(0, s.length % 1000))
|
|
);
|
|
}
|
|
|
|
function hitTest({button, target, offsetX, offsetY}) {
|
|
if (button) return;
|
|
const swatch = target.closest('.' + COLORVIEW_CLASS);
|
|
if (!swatch) return;
|
|
const {left, width, height} = getComputedStyle(swatch, '::before');
|
|
const bounds = swatch.getBoundingClientRect();
|
|
const swatchClicked =
|
|
offsetX >= parseFloat(left) - 1 &&
|
|
offsetX <= parseFloat(left) + parseFloat(width) + 1 &&
|
|
offsetY >= parseFloat(height) / 2 - bounds.height / 2 - 1 &&
|
|
offsetY <= parseFloat(height) / 2 + bounds.height / 2 + 1;
|
|
return swatchClicked && swatch;
|
|
}
|
|
|
|
//endregion
|
|
})();
|