diff options
17 files changed, 334 insertions, 20 deletions
diff --git a/subprojects/frontend/package.json b/subprojects/frontend/package.json index 73bb463d..970d00a3 100644 --- a/subprojects/frontend/package.json +++ b/subprojects/frontend/package.json | |||
@@ -55,6 +55,7 @@ | |||
55 | "ansi-styles": "^6.2.1", | 55 | "ansi-styles": "^6.2.1", |
56 | "csstype": "^3.1.3", | 56 | "csstype": "^3.1.3", |
57 | "d3": "^7.8.5", | 57 | "d3": "^7.8.5", |
58 | "d3-color": "^3.1.0", | ||
58 | "d3-graphviz": "patch:d3-graphviz@npm%3A5.3.0#~/.yarn/patches/d3-graphviz-npm-5.3.0-e0eace978a.patch", | 59 | "d3-graphviz": "patch:d3-graphviz@npm%3A5.3.0#~/.yarn/patches/d3-graphviz-npm-5.3.0-e0eace978a.patch", |
59 | "d3-selection": "^3.0.0", | 60 | "d3-selection": "^3.0.0", |
60 | "d3-zoom": "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch", | 61 | "d3-zoom": "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch", |
@@ -78,6 +79,7 @@ | |||
78 | "devDependencies": { | 79 | "devDependencies": { |
79 | "@lezer/generator": "^1.6.0", | 80 | "@lezer/generator": "^1.6.0", |
80 | "@types/d3": "^7.4.3", | 81 | "@types/d3": "^7.4.3", |
82 | "@types/d3-color": "^3.1.3", | ||
81 | "@types/d3-graphviz": "^2.6.10", | 83 | "@types/d3-graphviz": "^2.6.10", |
82 | "@types/d3-selection": "^3.0.10", | 84 | "@types/d3-selection": "^3.0.10", |
83 | "@types/d3-zoom": "^3.0.8", | 85 | "@types/d3-zoom": "^3.0.8", |
diff --git a/subprojects/frontend/src/editor/EditorArea.tsx b/subprojects/frontend/src/editor/EditorArea.tsx index aafaad40..ae5cff34 100644 --- a/subprojects/frontend/src/editor/EditorArea.tsx +++ b/subprojects/frontend/src/editor/EditorArea.tsx | |||
@@ -39,6 +39,7 @@ export default observer(function EditorArea({ | |||
39 | showLineNumbers={editorStore.showLineNumbers} | 39 | showLineNumbers={editorStore.showLineNumbers} |
40 | showActiveLine={!editorStore.hasSelection} | 40 | showActiveLine={!editorStore.hasSelection} |
41 | colorIdentifiers={editorStore.colorIdentifiers} | 41 | colorIdentifiers={editorStore.colorIdentifiers} |
42 | hexTypeHashes={editorStore.hexTypeHashes} | ||
42 | ref={editorParentRef} | 43 | ref={editorParentRef} |
43 | /> | 44 | /> |
44 | </Box> | 45 | </Box> |
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index 33bca382..f128d70d 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts | |||
@@ -111,6 +111,8 @@ export default class EditorStore { | |||
111 | 111 | ||
112 | unsavedChanges = false; | 112 | unsavedChanges = false; |
113 | 113 | ||
114 | hexTypeHashes: string[] = []; | ||
115 | |||
114 | constructor( | 116 | constructor( |
115 | initialValue: string, | 117 | initialValue: string, |
116 | pwaStore: PWAStore, | 118 | pwaStore: PWAStore, |
@@ -275,8 +277,12 @@ export default class EditorStore { | |||
275 | this.doCommand(nextDiagnostic); | 277 | this.doCommand(nextDiagnostic); |
276 | } | 278 | } |
277 | 279 | ||
278 | updateSemanticHighlighting(ranges: IHighlightRange[]): void { | 280 | updateSemanticHighlighting( |
281 | ranges: IHighlightRange[], | ||
282 | hexTypeHashes: string[], | ||
283 | ): void { | ||
279 | this.dispatch(setSemanticHighlighting(ranges)); | 284 | this.dispatch(setSemanticHighlighting(ranges)); |
285 | this.hexTypeHashes = hexTypeHashes; | ||
280 | } | 286 | } |
281 | 287 | ||
282 | updateOccurrences(write: IOccurrence[], read: IOccurrence[]): void { | 288 | updateOccurrences(write: IOccurrence[], read: IOccurrence[]): void { |
diff --git a/subprojects/frontend/src/editor/EditorTheme.ts b/subprojects/frontend/src/editor/EditorTheme.ts index 4978c7f7..6deda080 100644 --- a/subprojects/frontend/src/editor/EditorTheme.ts +++ b/subprojects/frontend/src/editor/EditorTheme.ts | |||
@@ -14,6 +14,7 @@ import { | |||
14 | type CSSObject, | 14 | type CSSObject, |
15 | type Theme, | 15 | type Theme, |
16 | } from '@mui/material/styles'; | 16 | } from '@mui/material/styles'; |
17 | import { lch } from 'd3-color'; | ||
17 | import { range } from 'lodash-es'; | 18 | import { range } from 'lodash-es'; |
18 | 19 | ||
19 | import svgURL from '../utils/svgURL'; | 20 | import svgURL from '../utils/svgURL'; |
@@ -21,6 +22,7 @@ import svgURL from '../utils/svgURL'; | |||
21 | function createTypeHashStyles( | 22 | function createTypeHashStyles( |
22 | theme: Theme, | 23 | theme: Theme, |
23 | colorIdentifiers: boolean, | 24 | colorIdentifiers: boolean, |
25 | hexTypeHashes: string[], | ||
24 | ): CSSObject { | 26 | ): CSSObject { |
25 | if (!colorIdentifiers) { | 27 | if (!colorIdentifiers) { |
26 | return {}; | 28 | return {}; |
@@ -34,6 +36,26 @@ function createTypeHashStyles( | |||
34 | }, | 36 | }, |
35 | }; | 37 | }; |
36 | }); | 38 | }); |
39 | hexTypeHashes.forEach((typeHash) => { | ||
40 | let color = lch(`#${typeHash}`); | ||
41 | if (theme.palette.mode === 'dark') { | ||
42 | color = color.brighter(); | ||
43 | if (color.l < 60) { | ||
44 | color.l = 60; | ||
45 | } | ||
46 | } else { | ||
47 | color = color.darker(); | ||
48 | if (color.l > 60) { | ||
49 | color.l = 60; | ||
50 | } | ||
51 | } | ||
52 | result[`.tok-problem-typeHash-_${typeHash}`] = { | ||
53 | '&, .tok-typeName': { | ||
54 | color: color.formatRgb(), | ||
55 | fontWeight: theme.typography.fontWeightEditorTypeHash, | ||
56 | }, | ||
57 | }; | ||
58 | }); | ||
37 | return result; | 59 | return result; |
38 | } | 60 | } |
39 | 61 | ||
@@ -42,12 +64,20 @@ export default styled('div', { | |||
42 | shouldForwardProp: (propName) => | 64 | shouldForwardProp: (propName) => |
43 | propName !== 'showLineNumbers' && | 65 | propName !== 'showLineNumbers' && |
44 | propName !== 'showActiveLine' && | 66 | propName !== 'showActiveLine' && |
45 | propName !== 'colorIdentifiers', | 67 | propName !== 'colorIdentifiers' && |
68 | propName !== 'hexTypeHashes', | ||
46 | })<{ | 69 | })<{ |
47 | showLineNumbers: boolean; | 70 | showLineNumbers: boolean; |
48 | showActiveLine: boolean; | 71 | showActiveLine: boolean; |
49 | colorIdentifiers: boolean; | 72 | colorIdentifiers: boolean; |
50 | }>(({ theme, showLineNumbers, showActiveLine, colorIdentifiers }) => { | 73 | hexTypeHashes: string[]; |
74 | }>(({ | ||
75 | theme, | ||
76 | showLineNumbers, | ||
77 | showActiveLine, | ||
78 | colorIdentifiers, | ||
79 | hexTypeHashes, | ||
80 | }) => { | ||
51 | const editorFontStyle: CSSObject = { | 81 | const editorFontStyle: CSSObject = { |
52 | ...theme.typography.editor, | 82 | ...theme.typography.editor, |
53 | fontWeight: theme.typography.fontWeightEditorNormal, | 83 | fontWeight: theme.typography.fontWeightEditorNormal, |
@@ -157,7 +187,7 @@ export default styled('div', { | |||
157 | fontStyle: 'normal', | 187 | fontStyle: 'normal', |
158 | }, | 188 | }, |
159 | }, | 189 | }, |
160 | ...createTypeHashStyles(theme, colorIdentifiers), | 190 | ...createTypeHashStyles(theme, colorIdentifiers, hexTypeHashes), |
161 | }; | 191 | }; |
162 | 192 | ||
163 | const matchingStyle: CSSObject = { | 193 | const matchingStyle: CSSObject = { |
diff --git a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx index cc8b5116..0980ea20 100644 --- a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx +++ b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx | |||
@@ -154,7 +154,13 @@ function DotGraphVisualizer({ | |||
154 | ], | 154 | ], |
155 | ); | 155 | ); |
156 | 156 | ||
157 | return <GraphTheme ref={setElement} colorNodes={graph.colorNodes} />; | 157 | return ( |
158 | <GraphTheme | ||
159 | ref={setElement} | ||
160 | colorNodes={graph.colorNodes} | ||
161 | hexTypeHashes={graph.hexTypeHashes} | ||
162 | /> | ||
163 | ); | ||
158 | } | 164 | } |
159 | 165 | ||
160 | DotGraphVisualizer.defaultProps = { | 166 | DotGraphVisualizer.defaultProps = { |
diff --git a/subprojects/frontend/src/graph/GraphStore.ts b/subprojects/frontend/src/graph/GraphStore.ts index d9282326..301b4d86 100644 --- a/subprojects/frontend/src/graph/GraphStore.ts +++ b/subprojects/frontend/src/graph/GraphStore.ts | |||
@@ -49,6 +49,8 @@ export function isVisibilityAllowed( | |||
49 | return true; | 49 | return true; |
50 | } | 50 | } |
51 | 51 | ||
52 | const TYPE_HASH_HEX_PREFFIX = '_'; | ||
53 | |||
52 | export default class GraphStore { | 54 | export default class GraphStore { |
53 | semantics: SemanticsSuccessResult = { | 55 | semantics: SemanticsSuccessResult = { |
54 | nodes: [], | 56 | nodes: [], |
@@ -66,6 +68,10 @@ export default class GraphStore { | |||
66 | 68 | ||
67 | selectedSymbol: RelationMetadata | undefined; | 69 | selectedSymbol: RelationMetadata | undefined; |
68 | 70 | ||
71 | hexTypeHashes: string[] = []; | ||
72 | |||
73 | private typeHashesMap = new Map<string, number>(); | ||
74 | |||
69 | constructor( | 75 | constructor( |
70 | private readonly editorStore: EditorStore, | 76 | private readonly editorStore: EditorStore, |
71 | private readonly nameOverride?: string, | 77 | private readonly nameOverride?: string, |
@@ -188,6 +194,36 @@ export default class GraphStore { | |||
188 | this.visibility.delete(key); | 194 | this.visibility.delete(key); |
189 | }); | 195 | }); |
190 | this.setSelectedSymbol(this.selectedSymbol); | 196 | this.setSelectedSymbol(this.selectedSymbol); |
197 | this.updateTypeHashes(); | ||
198 | } | ||
199 | |||
200 | /** | ||
201 | * Maintains a list of past and current color codes to avoid flashing | ||
202 | * when the graph view updates. | ||
203 | * | ||
204 | * As long as the previously used colors are still in in `typeHashesMap`, | ||
205 | * the view will not flash while Graphviz is recomputing, because we'll | ||
206 | * keep emitting styles for the colors. | ||
207 | */ | ||
208 | private updateTypeHashes(): void { | ||
209 | this.semantics.nodes.forEach(({ typeHash }) => { | ||
210 | if ( | ||
211 | typeHash !== undefined && | ||
212 | typeHash.startsWith(TYPE_HASH_HEX_PREFFIX) | ||
213 | ) { | ||
214 | const key = typeHash.substring(TYPE_HASH_HEX_PREFFIX.length); | ||
215 | this.typeHashesMap.set(key, 0); | ||
216 | } | ||
217 | }); | ||
218 | this.hexTypeHashes = Array.from(this.typeHashesMap.keys()); | ||
219 | this.hexTypeHashes.forEach((typeHash) => { | ||
220 | const age = this.typeHashesMap.get(typeHash); | ||
221 | if (age !== undefined && age < 10) { | ||
222 | this.typeHashesMap.set(typeHash, age + 1); | ||
223 | } else { | ||
224 | this.typeHashesMap.delete(typeHash); | ||
225 | } | ||
226 | }); | ||
191 | } | 227 | } |
192 | 228 | ||
193 | get colorNodes(): boolean { | 229 | get colorNodes(): boolean { |
diff --git a/subprojects/frontend/src/graph/GraphTheme.tsx b/subprojects/frontend/src/graph/GraphTheme.tsx index 34954345..50a003e0 100644 --- a/subprojects/frontend/src/graph/GraphTheme.tsx +++ b/subprojects/frontend/src/graph/GraphTheme.tsx | |||
@@ -13,10 +13,13 @@ import { | |||
13 | type CSSObject, | 13 | type CSSObject, |
14 | type Theme, | 14 | type Theme, |
15 | } from '@mui/material/styles'; | 15 | } from '@mui/material/styles'; |
16 | import { lch } from 'd3-color'; | ||
16 | import { range } from 'lodash-es'; | 17 | import { range } from 'lodash-es'; |
17 | 18 | ||
18 | import svgURL from '../utils/svgURL'; | 19 | import svgURL from '../utils/svgURL'; |
19 | 20 | ||
21 | import obfuscateColor from './obfuscateColor'; | ||
22 | |||
20 | function createEdgeColor( | 23 | function createEdgeColor( |
21 | suffix: string, | 24 | suffix: string, |
22 | stroke: string, | 25 | stroke: string, |
@@ -37,16 +40,32 @@ function createEdgeColor( | |||
37 | }; | 40 | }; |
38 | } | 41 | } |
39 | 42 | ||
40 | function createTypeHashStyles(theme: Theme, colorNodes: boolean): CSSObject { | 43 | function createTypeHashStyles( |
44 | theme: Theme, | ||
45 | colorNodes: boolean, | ||
46 | typeHashes: string[], | ||
47 | ): CSSObject { | ||
41 | if (!colorNodes) { | 48 | if (!colorNodes) { |
42 | return {}; | 49 | return {}; |
43 | } | 50 | } |
44 | const result: CSSObject = {}; | 51 | const result: CSSObject = {}; |
45 | range(theme.palette.highlight.typeHash.length).forEach((i) => { | 52 | range(theme.palette.highlight.typeHash.length).forEach((i) => { |
46 | result[`.node-typeHash-${i} .node-header`] = { | 53 | result[`.node-typeHash-${obfuscateColor(i.toString(10))} .node-header`] = { |
47 | fill: theme.palette.highlight.typeHash[i]?.box, | 54 | fill: theme.palette.highlight.typeHash[i]?.box, |
48 | }; | 55 | }; |
49 | }); | 56 | }); |
57 | typeHashes.forEach((typeHash) => { | ||
58 | let color = lch(`#${typeHash}`); | ||
59 | if (theme.palette.mode === 'dark') { | ||
60 | color = color.darker(); | ||
61 | if (color.l > 50) { | ||
62 | color.l = 50; | ||
63 | } | ||
64 | } | ||
65 | result[`.node-typeHash-_${obfuscateColor(typeHash)} .node-header`] = { | ||
66 | fill: color.formatRgb(), | ||
67 | }; | ||
68 | }); | ||
50 | return result; | 69 | return result; |
51 | } | 70 | } |
52 | 71 | ||
@@ -69,10 +88,12 @@ function iconStyle( | |||
69 | export function createGraphTheme({ | 88 | export function createGraphTheme({ |
70 | theme, | 89 | theme, |
71 | colorNodes, | 90 | colorNodes, |
91 | hexTypeHashes, | ||
72 | noEmbedIcons, | 92 | noEmbedIcons, |
73 | }: { | 93 | }: { |
74 | theme: Theme; | 94 | theme: Theme; |
75 | colorNodes: boolean; | 95 | colorNodes: boolean; |
96 | hexTypeHashes: string[]; | ||
76 | noEmbedIcons?: boolean; | 97 | noEmbedIcons?: boolean; |
77 | }): CSSObject { | 98 | }): CSSObject { |
78 | const shadowAlapha = theme.palette.mode === 'dark' ? 0.32 : 0.24; | 99 | const shadowAlapha = theme.palette.mode === 'dark' ? 0.32 : 0.24; |
@@ -111,7 +132,7 @@ export function createGraphTheme({ | |||
111 | '.node-exists-UNKNOWN .node-outline': { | 132 | '.node-exists-UNKNOWN .node-outline': { |
112 | strokeDasharray: '5 2', | 133 | strokeDasharray: '5 2', |
113 | }, | 134 | }, |
114 | ...createTypeHashStyles(theme, colorNodes), | 135 | ...createTypeHashStyles(theme, colorNodes, hexTypeHashes), |
115 | '.edge': { | 136 | '.edge': { |
116 | '& text': { | 137 | '& text': { |
117 | fontFamily: theme.typography.fontFamily, | 138 | fontFamily: theme.typography.fontFamily, |
@@ -155,7 +176,9 @@ export function createGraphTheme({ | |||
155 | 176 | ||
156 | export default styled('div', { | 177 | export default styled('div', { |
157 | name: 'GraphTheme', | 178 | name: 'GraphTheme', |
158 | })<{ colorNodes: boolean }>((args) => ({ | 179 | shouldForwardProp: (prop) => |
180 | prop !== 'colorNodes' && prop !== 'hexTypeHashes', | ||
181 | })<{ colorNodes: boolean; hexTypeHashes: string[] }>((args) => ({ | ||
159 | '& svg': { | 182 | '& svg': { |
160 | userSelect: 'none', | 183 | userSelect: 'none', |
161 | ...createGraphTheme(args), | 184 | ...createGraphTheme(args), |
diff --git a/subprojects/frontend/src/graph/dotSource.ts b/subprojects/frontend/src/graph/dotSource.ts index 3ac5eb1c..bcd386cf 100644 --- a/subprojects/frontend/src/graph/dotSource.ts +++ b/subprojects/frontend/src/graph/dotSource.ts | |||
@@ -10,6 +10,7 @@ import type { | |||
10 | } from '../xtext/xtextServiceResults'; | 10 | } from '../xtext/xtextServiceResults'; |
11 | 11 | ||
12 | import type GraphStore from './GraphStore'; | 12 | import type GraphStore from './GraphStore'; |
13 | import obfuscateColor from './obfuscateColor'; | ||
13 | 14 | ||
14 | const EDGE_WEIGHT = 1; | 15 | const EDGE_WEIGHT = 1; |
15 | const CONTAINMENT_WEIGHT = 5; | 16 | const CONTAINMENT_WEIGHT = 5; |
@@ -143,7 +144,7 @@ function createNodes( | |||
143 | classList.push('node-empty'); | 144 | classList.push('node-empty'); |
144 | } | 145 | } |
145 | if (node.typeHash !== undefined) { | 146 | if (node.typeHash !== undefined) { |
146 | classList.push(`node-typeHash-${node.typeHash}`); | 147 | classList.push(`node-typeHash-${obfuscateColor(node.typeHash)}`); |
147 | } | 148 | } |
148 | const classes = classList.join(' '); | 149 | const classes = classList.join(' '); |
149 | const name = nodeName(graph, node); | 150 | const name = nodeName(graph, node); |
diff --git a/subprojects/frontend/src/graph/export/exportDiagram.tsx b/subprojects/frontend/src/graph/export/exportDiagram.tsx index 44489d28..6abbcfdf 100644 --- a/subprojects/frontend/src/graph/export/exportDiagram.tsx +++ b/subprojects/frontend/src/graph/export/exportDiagram.tsx | |||
@@ -147,6 +147,7 @@ function appendStyles( | |||
147 | svg: SVGSVGElement, | 147 | svg: SVGSVGElement, |
148 | theme: Theme, | 148 | theme: Theme, |
149 | colorNodes: boolean, | 149 | colorNodes: boolean, |
150 | hexTypeHashes: string[], | ||
150 | fontsCSS: string, | 151 | fontsCSS: string, |
151 | ): void { | 152 | ): void { |
152 | const cache = createCache({ | 153 | const cache = createCache({ |
@@ -159,6 +160,7 @@ function appendStyles( | |||
159 | const styles = serializeStyles([createGraphTheme], cache.registered, { | 160 | const styles = serializeStyles([createGraphTheme], cache.registered, { |
160 | theme, | 161 | theme, |
161 | colorNodes, | 162 | colorNodes, |
163 | hexTypeHashes, | ||
162 | noEmbedIcons: true, | 164 | noEmbedIcons: true, |
163 | }); | 165 | }); |
164 | const rules: string[] = [fontsCSS]; | 166 | const rules: string[] = [fontsCSS]; |
@@ -336,7 +338,14 @@ export default async function exportDiagram( | |||
336 | } else if (settings.format === 'svg' && settings.embedFonts) { | 338 | } else if (settings.format === 'svg' && settings.embedFonts) { |
337 | fontsCSS = await fetchFontCSS(); | 339 | fontsCSS = await fetchFontCSS(); |
338 | } | 340 | } |
339 | appendStyles(svgDocument, copyOfSVG, theme, colorNodes, fontsCSS); | 341 | appendStyles( |
342 | svgDocument, | ||
343 | copyOfSVG, | ||
344 | theme, | ||
345 | colorNodes, | ||
346 | graph.hexTypeHashes, | ||
347 | fontsCSS, | ||
348 | ); | ||
340 | 349 | ||
341 | if (settings.format === 'pdf') { | 350 | if (settings.format === 'pdf') { |
342 | const pdf = await serializePDF(copyOfSVG, settings); | 351 | const pdf = await serializePDF(copyOfSVG, settings); |
diff --git a/subprojects/frontend/src/graph/obfuscateColor.ts b/subprojects/frontend/src/graph/obfuscateColor.ts new file mode 100644 index 00000000..57c15804 --- /dev/null +++ b/subprojects/frontend/src/graph/obfuscateColor.ts | |||
@@ -0,0 +1,21 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023-2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | const regExp = /\d/g; | ||
8 | const offset = 'g'.charCodeAt(0) - '0'.charCodeAt(0); | ||
9 | |||
10 | /* | ||
11 | * The SVG animation framework we use garbles all numbers while interpolating, | ||
12 | * so we mask numbers in hex color codes by replacing them with letters. | ||
13 | * | ||
14 | * @param color The hex code. | ||
15 | * @return The hex code with no number characters. | ||
16 | */ | ||
17 | export default function obfuscateColor(color: string): string { | ||
18 | return color.replaceAll(regExp, (match) => | ||
19 | String.fromCharCode(match.charCodeAt(0) + offset), | ||
20 | ); | ||
21 | } | ||
diff --git a/subprojects/frontend/src/xtext/HighlightingService.ts b/subprojects/frontend/src/xtext/HighlightingService.ts index 447f1401..eacee117 100644 --- a/subprojects/frontend/src/xtext/HighlightingService.ts +++ b/subprojects/frontend/src/xtext/HighlightingService.ts | |||
@@ -10,6 +10,8 @@ import type { IHighlightRange } from '../editor/semanticHighlighting'; | |||
10 | import type UpdateService from './UpdateService'; | 10 | import type UpdateService from './UpdateService'; |
11 | import { highlightingResult } from './xtextServiceResults'; | 11 | import { highlightingResult } from './xtextServiceResults'; |
12 | 12 | ||
13 | const TYPE_HASH_HEX_PREFIX = 'typeHash-_'; | ||
14 | |||
13 | export default class HighlightingService { | 15 | export default class HighlightingService { |
14 | constructor( | 16 | constructor( |
15 | private readonly store: EditorStore, | 17 | private readonly store: EditorStore, |
@@ -20,6 +22,7 @@ export default class HighlightingService { | |||
20 | const { regions } = highlightingResult.parse(push); | 22 | const { regions } = highlightingResult.parse(push); |
21 | const allChanges = this.updateService.computeChangesSinceLastUpdate(); | 23 | const allChanges = this.updateService.computeChangesSinceLastUpdate(); |
22 | const ranges: IHighlightRange[] = []; | 24 | const ranges: IHighlightRange[] = []; |
25 | const hexTypeHashes = new Set<string>(); | ||
23 | regions.forEach(({ offset, length, styleClasses }) => { | 26 | regions.forEach(({ offset, length, styleClasses }) => { |
24 | if (styleClasses.length === 0) { | 27 | if (styleClasses.length === 0) { |
25 | return; | 28 | return; |
@@ -34,11 +37,16 @@ export default class HighlightingService { | |||
34 | to, | 37 | to, |
35 | classes: styleClasses, | 38 | classes: styleClasses, |
36 | }); | 39 | }); |
40 | styleClasses.forEach((styleClass) => { | ||
41 | if (styleClass.startsWith(TYPE_HASH_HEX_PREFIX)) { | ||
42 | hexTypeHashes.add(styleClass.substring(TYPE_HASH_HEX_PREFIX.length)); | ||
43 | } | ||
44 | }); | ||
37 | }); | 45 | }); |
38 | this.store.updateSemanticHighlighting(ranges); | 46 | this.store.updateSemanticHighlighting(ranges, Array.from(hexTypeHashes)); |
39 | } | 47 | } |
40 | 48 | ||
41 | onDisconnect(): void { | 49 | onDisconnect(): void { |
42 | this.store.updateSemanticHighlighting([]); | 50 | this.store.updateSemanticHighlighting([], []); |
43 | } | 51 | } |
44 | } | 52 | } |
diff --git a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/TypeHashProvider.java b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/TypeHashProvider.java index dd9f1053..82a6af06 100644 --- a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/TypeHashProvider.java +++ b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/TypeHashProvider.java | |||
@@ -5,16 +5,17 @@ | |||
5 | */ | 5 | */ |
6 | package tools.refinery.language.ide.syntaxcoloring; | 6 | package tools.refinery.language.ide.syntaxcoloring; |
7 | 7 | ||
8 | import com.google.common.collect.ImmutableMap; | ||
9 | import com.google.inject.Inject; | 8 | import com.google.inject.Inject; |
10 | import com.google.inject.Singleton; | 9 | import com.google.inject.Singleton; |
11 | import org.eclipse.xtext.EcoreUtil2; | 10 | import org.eclipse.xtext.EcoreUtil2; |
12 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | 11 | import org.eclipse.xtext.naming.IQualifiedNameConverter; |
13 | import org.eclipse.xtext.naming.IQualifiedNameProvider; | 12 | import org.eclipse.xtext.naming.IQualifiedNameProvider; |
13 | import org.eclipse.xtext.resource.IEObjectDescription; | ||
14 | import org.eclipse.xtext.resource.IResourceDescription; | 14 | import org.eclipse.xtext.resource.IResourceDescription; |
15 | import org.eclipse.xtext.scoping.IScopeProvider; | 15 | import org.eclipse.xtext.scoping.IScopeProvider; |
16 | import org.eclipse.xtext.scoping.impl.GlobalResourceDescriptionProvider; | 16 | import org.eclipse.xtext.scoping.impl.GlobalResourceDescriptionProvider; |
17 | import org.eclipse.xtext.util.IResourceScopeCache; | 17 | import org.eclipse.xtext.util.IResourceScopeCache; |
18 | import tools.refinery.language.documentation.DocumentationCommentParser; | ||
18 | import tools.refinery.language.model.problem.*; | 19 | import tools.refinery.language.model.problem.*; |
19 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; | 20 | import tools.refinery.language.resource.ProblemResourceDescriptionStrategy; |
20 | import tools.refinery.language.scoping.imports.ImportCollector; | 21 | import tools.refinery.language.scoping.imports.ImportCollector; |
@@ -65,21 +66,30 @@ public class TypeHashProvider { | |||
65 | 66 | ||
66 | private Map<String, String> computeHashes(Problem problem) { | 67 | private Map<String, String> computeHashes(Problem problem) { |
67 | var resourceDescriptions = getResourceDescriptions(problem); | 68 | var resourceDescriptions = getResourceDescriptions(problem); |
69 | var map = new HashMap<String, String>(); | ||
68 | var qualifiedNameStrings = new TreeSet<String>(); | 70 | var qualifiedNameStrings = new TreeSet<String>(); |
69 | for (var resourceDescription : resourceDescriptions) { | 71 | for (var resourceDescription : resourceDescriptions) { |
70 | for (var description : resourceDescription.getExportedObjectsByType(ProblemPackage.Literals.RELATION)) { | 72 | for (var description : resourceDescription.getExportedObjectsByType(ProblemPackage.Literals.RELATION)) { |
71 | if (ProblemResourceDescriptionStrategy.COLOR_RELATION_TRUE.equals( | 73 | if (ProblemResourceDescriptionStrategy.COLOR_RELATION_TRUE.equals( |
72 | description.getUserData(ProblemResourceDescriptionStrategy.COLOR_RELATION))) { | 74 | description.getUserData(ProblemResourceDescriptionStrategy.COLOR_RELATION))) { |
73 | var qualifiedNameString = qualifiedNameConverter.toString(description.getQualifiedName()); | 75 | var qualifiedNameString = qualifiedNameConverter.toString(description.getQualifiedName()); |
76 | var presetColor = getPresetColor(description); | ||
77 | if (presetColor != null) { | ||
78 | map.put(qualifiedNameString, presetColor); | ||
79 | } | ||
74 | qualifiedNameStrings.add(qualifiedNameString); | 80 | qualifiedNameStrings.add(qualifiedNameString); |
75 | } | 81 | } |
76 | } | 82 | } |
77 | } | 83 | } |
78 | var stringList = new ArrayList<>(qualifiedNameStrings); | 84 | var stringList = new ArrayList<>(qualifiedNameStrings); |
79 | int size = stringList.size(); | 85 | int size = stringList.size(); |
80 | if (size == 0) { | 86 | if (size != 0) { |
81 | return Map.of(); | 87 | shuffleColors(size, stringList, map); |
82 | } | 88 | } |
89 | return Collections.unmodifiableMap(map); | ||
90 | } | ||
91 | |||
92 | private static void shuffleColors(int size, ArrayList<String> stringList, Map<String, String> map) { | ||
83 | // The use of a non-cryptographic random generator is safe here, because we only use it to shuffle the color | 93 | // The use of a non-cryptographic random generator is safe here, because we only use it to shuffle the color |
84 | // IDs in a pseudo-random way. The shuffle depends on the size of the list of identifiers before padding to | 94 | // IDs in a pseudo-random way. The shuffle depends on the size of the list of identifiers before padding to |
85 | // make sure that adding a new class randomizes all color IDs. | 95 | // make sure that adding a new class randomizes all color IDs. |
@@ -91,15 +101,13 @@ public class TypeHashProvider { | |||
91 | } | 101 | } |
92 | size += padding; | 102 | size += padding; |
93 | Collections.shuffle(stringList, random); | 103 | Collections.shuffle(stringList, random); |
94 | var mapBuilder = ImmutableMap.<String, String>builder(); | ||
95 | for (int i = 0; i < size; i++) { | 104 | for (int i = 0; i < size; i++) { |
96 | var key = stringList.get(i); | 105 | var key = stringList.get(i); |
97 | if (key != null) { | 106 | if (key != null) { |
98 | int colorId = i % COLOR_COUNT; | 107 | int colorId = i % COLOR_COUNT; |
99 | mapBuilder.put(key, Integer.toString(colorId)); | 108 | map.putIfAbsent(key, Integer.toString(colorId)); |
100 | } | 109 | } |
101 | } | 110 | } |
102 | return mapBuilder.build(); | ||
103 | } | 111 | } |
104 | 112 | ||
105 | private List<IResourceDescription> getResourceDescriptions(Problem problem) { | 113 | private List<IResourceDescription> getResourceDescriptions(Problem problem) { |
@@ -127,4 +135,8 @@ public class TypeHashProvider { | |||
127 | } | 135 | } |
128 | return resourceDescriptions; | 136 | return resourceDescriptions; |
129 | } | 137 | } |
138 | |||
139 | private String getPresetColor(IEObjectDescription description) { | ||
140 | return description.getUserData(DocumentationCommentParser.COLOR_TAG); | ||
141 | } | ||
130 | } | 142 | } |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java index f9a564b0..f7039027 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java +++ b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java | |||
@@ -12,6 +12,7 @@ package tools.refinery.language; | |||
12 | import com.google.inject.Binder; | 12 | import com.google.inject.Binder; |
13 | import com.google.inject.name.Names; | 13 | import com.google.inject.name.Names; |
14 | import org.eclipse.xtext.conversion.IValueConverterService; | 14 | import org.eclipse.xtext.conversion.IValueConverterService; |
15 | import org.eclipse.xtext.documentation.impl.AbstractMultiLineCommentProvider; | ||
15 | import org.eclipse.xtext.linking.ILinkingService; | 16 | import org.eclipse.xtext.linking.ILinkingService; |
16 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | 17 | import org.eclipse.xtext.naming.IQualifiedNameConverter; |
17 | import org.eclipse.xtext.naming.IQualifiedNameProvider; | 18 | import org.eclipse.xtext.naming.IQualifiedNameProvider; |
@@ -144,4 +145,11 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule { | |||
144 | public Class<? extends IDiagnosticConverter> bindIDiagnosticConverter() { | 145 | public Class<? extends IDiagnosticConverter> bindIDiagnosticConverter() { |
145 | return ProblemDiagnosticConverter.class; | 146 | return ProblemDiagnosticConverter.class; |
146 | } | 147 | } |
148 | |||
149 | public void configureAbstractMultiLineCommentProvider(Binder binder) { | ||
150 | // Only parse documentation tags from Javadoc-style comments. | ||
151 | binder.bind(String.class) | ||
152 | .annotatedWith(Names.named(AbstractMultiLineCommentProvider.START_TAG)) | ||
153 | .toInstance("/\\*\\*"); | ||
154 | } | ||
147 | } | 155 | } |
diff --git a/subprojects/language/src/main/java/tools/refinery/language/documentation/DocumentationCommentParser.java b/subprojects/language/src/main/java/tools/refinery/language/documentation/DocumentationCommentParser.java new file mode 100644 index 00000000..3586abd2 --- /dev/null +++ b/subprojects/language/src/main/java/tools/refinery/language/documentation/DocumentationCommentParser.java | |||
@@ -0,0 +1,42 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.documentation; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import com.google.inject.Singleton; | ||
10 | import org.eclipse.emf.ecore.EObject; | ||
11 | import org.eclipse.xtext.documentation.IEObjectDocumentationProvider; | ||
12 | |||
13 | import java.util.Locale; | ||
14 | import java.util.Map; | ||
15 | import java.util.regex.Pattern; | ||
16 | |||
17 | @Singleton | ||
18 | public class DocumentationCommentParser { | ||
19 | private static final String PREFIX = "tools.refinery.language.documentation.DocumentationCommentParser."; | ||
20 | public static final String COLOR_TAG = PREFIX + "COLOR_TAG"; | ||
21 | |||
22 | private static final Pattern COLOR_PATTERN = Pattern.compile( | ||
23 | "(?m)^@color[ \t]+(\\d|#[\\da-fA-F]{6}|#[\\da-fA-F]{3})"); | ||
24 | |||
25 | @Inject | ||
26 | private IEObjectDocumentationProvider documentationProvider; | ||
27 | |||
28 | public Map<String, String> parseDocumentation(EObject eObject) { | ||
29 | var documentation = documentationProvider.getDocumentation(eObject); | ||
30 | if (documentation == null) { | ||
31 | return Map.of(); | ||
32 | } | ||
33 | var colorMatch = COLOR_PATTERN.matcher(documentation); | ||
34 | if (colorMatch.find()) { | ||
35 | // Use a {@code _} instead of a {@code #} to signify hex codes, because the type hashes have to be valid | ||
36 | // CSS class names. | ||
37 | var color = colorMatch.group(1).toLowerCase(Locale.ROOT).replace("#", "_"); | ||
38 | return Map.of(COLOR_TAG, color); | ||
39 | } | ||
40 | return Map.of(); | ||
41 | } | ||
42 | } | ||
diff --git a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java index 3080a78e..3dcf6b1f 100644 --- a/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java +++ b/subprojects/language/src/main/java/tools/refinery/language/resource/ProblemResourceDescriptionStrategy.java | |||
@@ -18,6 +18,7 @@ import org.eclipse.xtext.resource.EObjectDescription; | |||
18 | import org.eclipse.xtext.resource.IEObjectDescription; | 18 | import org.eclipse.xtext.resource.IEObjectDescription; |
19 | import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionStrategy; | 19 | import org.eclipse.xtext.resource.impl.DefaultResourceDescriptionStrategy; |
20 | import org.eclipse.xtext.util.IAcceptor; | 20 | import org.eclipse.xtext.util.IAcceptor; |
21 | import tools.refinery.language.documentation.DocumentationCommentParser; | ||
21 | import tools.refinery.language.naming.ProblemQualifiedNameProvider; | 22 | import tools.refinery.language.naming.ProblemQualifiedNameProvider; |
22 | import tools.refinery.language.scoping.imports.ImportCollector; | 23 | import tools.refinery.language.scoping.imports.ImportCollector; |
23 | import tools.refinery.language.model.problem.*; | 24 | import tools.refinery.language.model.problem.*; |
@@ -56,6 +57,9 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti | |||
56 | @Inject | 57 | @Inject |
57 | private ImportCollector importCollector; | 58 | private ImportCollector importCollector; |
58 | 59 | ||
60 | @Inject | ||
61 | private DocumentationCommentParser documentationCommentParser; | ||
62 | |||
59 | @Override | 63 | @Override |
60 | public boolean createEObjectDescriptions(EObject eObject, IAcceptor<IEObjectDescription> acceptor) { | 64 | public boolean createEObjectDescriptions(EObject eObject, IAcceptor<IEObjectDescription> acceptor) { |
61 | if (!shouldExport(eObject)) { | 65 | if (!shouldExport(eObject)) { |
@@ -154,6 +158,8 @@ public class ProblemResourceDescriptionStrategy extends DefaultResourceDescripti | |||
154 | if (eObject instanceof PredicateDefinition predicateDefinition && predicateDefinition.isError()) { | 158 | if (eObject instanceof PredicateDefinition predicateDefinition && predicateDefinition.isError()) { |
155 | builder.put(ERROR_PREDICATE, ERROR_PREDICATE_TRUE); | 159 | builder.put(ERROR_PREDICATE, ERROR_PREDICATE_TRUE); |
156 | } | 160 | } |
161 | var documentationMap = documentationCommentParser.parseDocumentation(eObject); | ||
162 | builder.putAll(documentationMap); | ||
157 | return builder.build(); | 163 | return builder.build(); |
158 | } | 164 | } |
159 | 165 | ||
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/documentation/DocumentationCommentParserTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/documentation/DocumentationCommentParserTest.java new file mode 100644 index 00000000..0566e7e2 --- /dev/null +++ b/subprojects/language/src/test/java/tools/refinery/language/tests/documentation/DocumentationCommentParserTest.java | |||
@@ -0,0 +1,94 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.tests.documentation; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import org.eclipse.xtext.testing.InjectWith; | ||
10 | import org.eclipse.xtext.testing.extensions.InjectionExtension; | ||
11 | import org.junit.jupiter.api.extension.ExtendWith; | ||
12 | import org.junit.jupiter.params.ParameterizedTest; | ||
13 | import org.junit.jupiter.params.provider.Arguments; | ||
14 | import org.junit.jupiter.params.provider.MethodSource; | ||
15 | import tools.refinery.language.documentation.DocumentationCommentParser; | ||
16 | import tools.refinery.language.model.tests.utils.ProblemParseHelper; | ||
17 | import tools.refinery.language.tests.ProblemInjectorProvider; | ||
18 | |||
19 | import java.util.stream.Stream; | ||
20 | |||
21 | import static org.hamcrest.MatcherAssert.assertThat; | ||
22 | import static org.hamcrest.Matchers.is; | ||
23 | |||
24 | @ExtendWith(InjectionExtension.class) | ||
25 | @InjectWith(ProblemInjectorProvider.class) | ||
26 | class DocumentationCommentParserTest { | ||
27 | @Inject | ||
28 | private ProblemParseHelper parseHelper; | ||
29 | |||
30 | @Inject | ||
31 | private DocumentationCommentParser commentParser; | ||
32 | |||
33 | @ParameterizedTest | ||
34 | @MethodSource | ||
35 | void colorTest(String text, String expectedColor) { | ||
36 | var parseResult = parseHelper.parse(text); | ||
37 | var foo = parseResult.findClass("Foo").classDeclaration(); | ||
38 | var documentation = commentParser.parseDocumentation(foo); | ||
39 | var actualColor = documentation.get(DocumentationCommentParser.COLOR_TAG); | ||
40 | assertThat(actualColor, is(expectedColor)); | ||
41 | } | ||
42 | |||
43 | static Stream<Arguments> colorTest() { | ||
44 | return Stream.of( | ||
45 | Arguments.of("class Foo.", null), | ||
46 | Arguments.of(""" | ||
47 | % @color #ff0000 | ||
48 | class Foo. | ||
49 | """, null), | ||
50 | Arguments.of(""" | ||
51 | /* | ||
52 | * @color #ff0000 | ||
53 | */ | ||
54 | class Foo. | ||
55 | """, null), | ||
56 | Arguments.of(""" | ||
57 | /** | ||
58 | * @color #ff0000 | ||
59 | */ | ||
60 | class Foo. | ||
61 | """, "_ff0000"), | ||
62 | Arguments.of(""" | ||
63 | /** | ||
64 | * @color #ff0000 other | ||
65 | */ | ||
66 | class Foo. | ||
67 | """, "_ff0000"), | ||
68 | Arguments.of(""" | ||
69 | /** @color #ff0000 */ | ||
70 | class Foo. | ||
71 | """, "_ff0000"), | ||
72 | Arguments.of(""" | ||
73 | /**@color #ff0000*/ | ||
74 | class Foo. | ||
75 | """, "_ff0000"), | ||
76 | Arguments.of(""" | ||
77 | /**@color\t #ff0000*/ | ||
78 | class Foo. | ||
79 | """, "_ff0000"), | ||
80 | Arguments.of(""" | ||
81 | /** @color #F2af00 */ | ||
82 | class Foo. | ||
83 | """, "_f2af00"), | ||
84 | Arguments.of(""" | ||
85 | /** @color #Fa0 */ | ||
86 | class Foo. | ||
87 | """, "_fa0"), | ||
88 | Arguments.of(""" | ||
89 | /** @color 4 */ | ||
90 | class Foo. | ||
91 | """, "4") | ||
92 | ); | ||
93 | } | ||
94 | } | ||
@@ -2192,6 +2192,7 @@ __metadata: | |||
2192 | "@mui/system": "npm:^5.15.11" | 2192 | "@mui/system": "npm:^5.15.11" |
2193 | "@mui/x-data-grid": "npm:^6.19.5" | 2193 | "@mui/x-data-grid": "npm:^6.19.5" |
2194 | "@types/d3": "npm:^7.4.3" | 2194 | "@types/d3": "npm:^7.4.3" |
2195 | "@types/d3-color": "npm:^3.1.3" | ||
2195 | "@types/d3-graphviz": "npm:^2.6.10" | 2196 | "@types/d3-graphviz": "npm:^2.6.10" |
2196 | "@types/d3-selection": "npm:^3.0.10" | 2197 | "@types/d3-selection": "npm:^3.0.10" |
2197 | "@types/d3-zoom": "npm:^3.0.8" | 2198 | "@types/d3-zoom": "npm:^3.0.8" |
@@ -2213,6 +2214,7 @@ __metadata: | |||
2213 | cross-env: "npm:^7.0.3" | 2214 | cross-env: "npm:^7.0.3" |
2214 | csstype: "npm:^3.1.3" | 2215 | csstype: "npm:^3.1.3" |
2215 | d3: "npm:^7.8.5" | 2216 | d3: "npm:^7.8.5" |
2217 | d3-color: "npm:^3.1.0" | ||
2216 | d3-graphviz: "patch:d3-graphviz@npm%3A5.3.0#~/.yarn/patches/d3-graphviz-npm-5.3.0-e0eace978a.patch" | 2218 | d3-graphviz: "patch:d3-graphviz@npm%3A5.3.0#~/.yarn/patches/d3-graphviz-npm-5.3.0-e0eace978a.patch" |
2217 | d3-selection: "npm:^3.0.0" | 2219 | d3-selection: "npm:^3.0.0" |
2218 | d3-zoom: "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch" | 2220 | d3-zoom: "patch:d3-zoom@npm%3A3.0.0#~/.yarn/patches/d3-zoom-npm-3.0.0-18f706a421.patch" |
@@ -2607,6 +2609,13 @@ __metadata: | |||
2607 | languageName: node | 2609 | languageName: node |
2608 | linkType: hard | 2610 | linkType: hard |
2609 | 2611 | ||
2612 | "@types/d3-color@npm:^3.1.3": | ||
2613 | version: 3.1.3 | ||
2614 | resolution: "@types/d3-color@npm:3.1.3" | ||
2615 | checksum: 10c0/65eb0487de606eb5ad81735a9a5b3142d30bc5ea801ed9b14b77cb14c9b909f718c059f13af341264ee189acf171508053342142bdf99338667cea26a2d8d6ae | ||
2616 | languageName: node | ||
2617 | linkType: hard | ||
2618 | |||
2610 | "@types/d3-contour@npm:*": | 2619 | "@types/d3-contour@npm:*": |
2611 | version: 3.0.2 | 2620 | version: 3.0.2 |
2612 | resolution: "@types/d3-contour@npm:3.0.2" | 2621 | resolution: "@types/d3-contour@npm:3.0.2" |
@@ -4133,7 +4142,7 @@ __metadata: | |||
4133 | languageName: node | 4142 | languageName: node |
4134 | linkType: hard | 4143 | linkType: hard |
4135 | 4144 | ||
4136 | "d3-color@npm:1 - 3, d3-color@npm:3": | 4145 | "d3-color@npm:1 - 3, d3-color@npm:3, d3-color@npm:^3.1.0": |
4137 | version: 3.1.0 | 4146 | version: 3.1.0 |
4138 | resolution: "d3-color@npm:3.1.0" | 4147 | resolution: "d3-color@npm:3.1.0" |
4139 | checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c | 4148 | checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c |