diff --git a/CHANGELOG.md b/CHANGELOG.md index fcf6925..bfd6407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,11 @@ ## next - Added `navbuttons.uploadFromClipboard` button -- Changed `struct` view to avoid auto expading numeric arrays +- Added `marks` option for `source` view, which allows injecting visual or text marks at specific points in the source text +- Renamed the `refs` option in the `source` view to `ranges` +- Updated the `type` field of entries in the `ranges` option (previously `refs`) to be optional. If the `href` field is specified, the `type` value defaults to `link`; otherwise, it defaults to `span` instead of `spotlight`. The `spotlight` type is now replaced by `span` - Fixed default copy text button action in `source` view +- Changed `struct` view to avoid auto expading numeric arrays ## 1.0.0-beta.92 (18-12-2024) diff --git a/src/views/text/source.css b/src/views/text/source.css index 75ed999..d1b9c93 100644 --- a/src/views/text/source.css +++ b/src/views/text/source.css @@ -126,44 +126,133 @@ color: var(--discovery-fmt-color); } -.view-source__source .spotlight { +.view-source__source .spotlight, +.view-source__source .mark { + --bg-color: light-dark(#f5e69a, #515143); + --color: light-dark(#948634, #bdb06a); + background: #f5e69a; - background: light-dark(#f5e69a, #515143); + background: var(--bg-color); background-image: linear-gradient(to right, light-dark(#fafafa80, transparent), light-dark(#fafafa80, transparent) ); - color: light-dark(#948634, #bdb06a); + color: var(--color); padding: 1px 1px 2px; margin: 0 -1px; } -.view-source__source .spotlight.error { - background-color: light-dark(#ffc9c9, #603c3c); - color: light-dark(#bb6665, #ed9e9d); +.view-source__source .mark { + margin-left: 0px; + margin-right: 2px; + padding: 1.5px .35em 2.5px; + font-family: var(--discovery-font-family); + font-size: 83%; + line-height: 1; + border: .5px solid currentColor; + border-color: color-mix(in srgb, currentColor 65%, var(--bg-color)); + border-radius: 4px; + background-color: light-dark(#e4e4e4, #2b2b2b); + background-clip: padding-box; + text-decoration-thickness: 1px; +} +.view-source__source .mark[data-kind=none] { + color: var(--discovery-color); + border-color: transparent; + background-clip: border-box; +} +.view-source__source .mark[data-kind=dot]::before { + --dot-color: var(--color); + content: ''; + display: inline-block; + vertical-align: middle; + padding: 4px; + margin: -2px 4px 0 1px; + font-size: 0px; + border-radius: 4px; + background-color: color-mix(in srgb, var(--dot-color) 85%, var(--discovery-background-color)); +} +.view-source__source .mark[data-kind=dot]:empty { + border: none; + background-color: transparent; + padding-left: 1px; + padding-right: 1px; +} +.view-source__source .mark[data-kind=dot]:empty::before { + margin-right: 1px; +} +.view-source__source .mark[data-kind=self] { + --color: light-dark(#4c934c, #6eae6e); } -.view-source__source .spotlight.def { - background-color: light-dark(#d3e7fb, #3a5066); - color: light-dark(#668fb8, #8cbae7); +.view-source__source .mark[data-kind=nested] { + --color: light-dark(#96944d, #aead6e); } -.view-source__source .spotlight.ref { - background-color: light-dark(#cee99b, #415220); - color: light-dark(#7a9a3b, #95ca2c); +.view-source__source .mark[data-kind=total] { + --color: light-dark(#9580bf, #aa92da); } -.view-source__source .spotlight.global-ref { - background-color: light-dark(#ffd8a3, #61492a); - color: light-dark(#9a7f59, #d8b381); +.view-source__source .mark[data-prefix]:not([data-kind=dot])::before { + content: attr(data-prefix); + display: inline-block; + color: color-mix(in srgb, currentColor 50%, var(--bg-color)); + padding: 0 3px 0 1px; +} +.view-source__source .mark[data-postfix]:not([data-kind=dot])::after { + content: attr(data-postfix); + display: inline-block; + color: color-mix(in srgb, currentColor 50%, var(--bg-color)); } -.view-source__source a.spotlight { + +.view-source__source .mark.inactive { + --color: #888; +} +.view-source__source .spotlight.error, +.view-source__source .mark.error { + --bg-color: light-dark(#ffc9c9, #603c3c); + --color: light-dark(#bb6665, #ed9e9d); +} +.view-source__source .spotlight.def, +.view-source__source .mark.def { + --bg-color: light-dark(#d3e7fb, #3a5066); + --color: light-dark(#668fb8, #8cbae7); +} +.view-source__source .spotlight.ref, +.view-source__source .mark.ref { + --bg-color: light-dark(#cee99b, #415220); + --color: light-dark(#7a9a3b, #95ca2c); +} +.view-source__source .spotlight.global-ref, +.view-source__source .mark.global-ref { + --bg-color: light-dark(#ffd8a3, #61492a); + --color: light-dark(#9a7f59, #d8b381); +} + +.view-source__source a.spotlight, +.view-source__source a.mark { position: relative; text-decoration-color: color-mix(in srgb, currentcolor 45%, transparent); + text-underline-offset: 1.5px; } -.view-source__source a.spotlight:hover { - text-decoration-color: color-mix(in srgb, currentcolor 70%, transparent); +.view-source__source a.spotlight:hover, +.view-source__source a.mark:hover { + text-decoration-color: color-mix(in srgb, currentcolor 75%, transparent); background-image: linear-gradient(to right, light-dark(transparent, #35353580), light-dark(transparent, #35353580) ); } +.view-source__source a.mark:hover, +.view-source__source .mark.discovery-view-has-tooltip:hover { + border-color: currentColor; +} +.view-source__source a.mark[data-kind=dot]:empty::before, +.view-source__source .mark.discovery-view-has-tooltip[data-kind=dot]:empty::before { + outline: 1px solid color-mix(in srgb, var(--dot-color) 45%, transparent); + outline-offset: 1px; +} +.view-source__source a.mark[data-kind=dot]:hover::before, +.view-source__source .mark.discovery-view-has-tooltip[data-kind=dot]:hover::before { + outline-color: var(--dot-color); +} + .view-source__source .spotlight-ignore { background: #ddd; background-image: repeating-linear-gradient(-45deg, diff --git a/src/views/text/source.js b/src/views/text/source.js index 0f09ef0..2f9c5cc 100644 --- a/src/views/text/source.js +++ b/src/views/text/source.js @@ -5,6 +5,7 @@ import CodeMirror from 'codemirror'; import 'codemirror/mode/javascript/javascript.js'; import 'codemirror/mode/css/css.js'; import 'codemirror/mode/xml/xml.js'; +import { escapeHtml } from '../../core/utils/html.js'; import { createElement } from '../../core/utils/dom.js'; import { copyText } from '../../core/utils/copy-text.js'; import usage from './source.usage.js'; @@ -56,19 +57,19 @@ function codeMirrorHighlight(modespec, host) { function classNames(options, defaultClassNames) { const customClassName = options && options.className; - const classNames = [ - defaultClassNames, - Array.isArray(customClassName) - ? customClassName.join(' ') - : (typeof customClassName === 'string' ? customClassName : false) - ].filter(Boolean).join(' '); + const resolvedClassName = Array.isArray(customClassName) + ? customClassName.join(' ') + : (typeof customClassName === 'string' ? customClassName : false); + const classNames = defaultClassNames && resolvedClassName + ? [defaultClassNames, resolvedClassName].join(' ') + : defaultClassNames || resolvedClassName || ''; return classNames ? ` class="${classNames}"` : ''; } -function refAttrs(data) { +function refAttrs(data, defaultClassName = 'spotlight') { return `${ - classNames(data, 'spotlight') + classNames(data, defaultClassName) }${ data.marker ? ` data-marker="${data.marker}"` : '' }${ @@ -76,32 +77,68 @@ function refAttrs(data) { }`; } +function markAttrs(data, defaultClassName = 'mark') { + const prefix = data.prefix ?? (['self', 'nested', 'total'].includes(data.kind) ? data.kind[0].toUpperCase() : undefined); + const postfix = data.postfix ?? undefined; + + return refAttrs(data, defaultClassName) + ` data-render-id=${data.renderId}${ + data.kind ? ` data-kind="${data.kind}"` : '' + }${ + prefix !== undefined ? ` data-prefix="${escapeHtml(prefix)}"` : '' + }${ + postfix !== undefined ? ` data-postfix="${escapeHtml(postfix)}"` : '' + }`; +} + const refsPrinter = { html: { open({ data }) { switch (data.type) { case 'link': - return ``; + return ``; + case 'span': case 'spotlight': - return ``; + return ``; } }, close({ data }) { switch (data.type) { case 'link': return ''; + case 'span': case 'spotlight': return ''; } } } }; +const marksPrinter = { + html: { + open({ data }) { + switch (data.type) { + case 'link': + return ``; + case 'span': + return ``; + } + }, + close({ data }) { + switch (data.type) { + case 'link': + return ''; + case 'span': + return ''; + } + } + } +}; const props = `is not array? | { source: #.props has no 'source' ? is string ?: content is string ? content : source, syntax, lineNum is function ?: is not undefined ? bool() : true, - refs is array ?: null, + ranges: #.props.refs or refs | is array ?: undefined, + marks is array ?: null, maxSourceSizeToHighlight is number ?: 250 * 1024, // 250Kb actionButtons: undefined, actionCopySource is undefined ? true, @@ -114,13 +151,16 @@ export default function(host) { const preludeEl = el.appendChild(createElement('div', 'view-source__prelude')); const contentEl = el.appendChild(createElement('div', 'view-source__content')); const postludeEl = el.appendChild(createElement('div', 'view-source__postlude')); - const refsTooltips = new Map(); + const viewTooltips = new Map(); + const markContentRenders = new Map(); + const markRenders = []; const decorators = []; const { source, syntax, lineNum = true, - refs, + ranges, + marks, maxSourceSizeToHighlight, actionButtons, actionCopySource, @@ -134,7 +174,8 @@ export default function(host) { source, syntax, lineNum, - refs, + ranges, + marks, maxSourceSizeToHighlight } }; @@ -157,27 +198,91 @@ export default function(host) { }]); } - if (Array.isArray(refs)) { + if (Array.isArray(ranges)) { decorators.push([ - (_, createRange) => refs.forEach(ref => { - if (ref.range) { - let tooltipId = undefined; - - if (ref.tooltip) { - refsTooltips.set(tooltipId = refsTooltips.size, ref); - } - - createRange( - ref.range[0], - ref.range[1], - { type: 'spotlight', ...ref, tooltipId } - ); + (_, createRange) => ranges.forEach(ref => { + if (!ref || typeof ref !== 'object') { + host.logger.warn('Bad value for an entry in "source" view props.ranges, must be an object', { props, entry: ref }); + return; + } + + const refType = ref.type + ? (ref.type === 'spotlight' ? 'span' : ref.type) + : (ref.href ? 'link' : 'span'); + const refRange = ref.range; + let tooltipId = undefined; + + if (!['link', 'span'].includes(refType)) { + host.logger.warn(`Bad type "${refType}" of an entry in "source" view props.ranges`, { props, ref }); + return; + } + + if (!refRange) { + host.logger.warn('Missed range for an entry in "source" view props.ranges', { props, entry: ref }); + return; } + + if (ref.tooltip) { + viewTooltips.set(tooltipId = viewTooltips.size, ref); + } + + createRange( + ref.range[0], + ref.range[1], + { ...ref, type: refType, tooltipId } + ); }), refsPrinter ]); } + if (Array.isArray(marks)) { + decorators.push([ + (_, createRange) => marks.forEach(mark => { + if (!mark || typeof mark !== 'object') { + host.logger.warn('Bad value for an entry in "source" view props.mark, must be an object', { props, mark }); + return; + } + + const markKind = !mark.content + ? 'dot' + : !mark.kind || ['dot', 'self', 'nested', 'total', 'none'].includes(mark.kind) + ? mark.kind || 'span' + : undefined; + const markOffset = typeof mark.offset === 'number' && isFinite(mark.offset) + ? Math.max(0, Math.round(mark.offset)) + : undefined; + let tooltipId = undefined; + + if (typeof markKind !== 'string') { + host.logger.warn('Bad "kind" value for an entry in "source" view props.marks', { props, mark }); + return; + } + + if (typeof markOffset !== 'number') { + host.logger.warn('Bad "offset" value for an entry in "source" view props.marks', { props, mark }); + return; + } + + if (mark.tooltip) { + viewTooltips.set(tooltipId = viewTooltips.size, mark); + } + + const markConfig = { + ...mark, + type: typeof mark.href === 'string' ? 'link' : 'span', + kind: markKind, + renderId: markContentRenders.size, + tooltipId + }; + + markContentRenders.set(markConfig.renderId, markConfig); + createRange(markOffset, markOffset, markConfig); + }), + marksPrinter + ]); + } + const lineOffset = typeof lineNum === 'function' ? lineNum : idx => idx + 1; const lines = lineNum ? '