mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 13:30:24 +08:00
Snapshot Markdown Improvements (#1555)
* basic markdown commands * markdown renderers * support markdown tables * fix style * indicate external links in markdown * simplify the api * load image from MVSX * lint * docs * typo * custom color palette support * move manager to mol-plugin-state * customize args parser * better custom args parser support
This commit is contained in:
@@ -15,6 +15,13 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Canvas node: support custom properties `molstar_enable_outline`, `molstar_enable_shadow`, `molstar_enable_ssao`
|
||||
- Renamed some color schemes ('inferno' -> 'inferno-no-black', 'magma' -> 'magma-no-black', 'turbo' -> 'turbo-no-black', 'rainbow' -> 'simple-rainbow')
|
||||
- Added new color schemes, synchronized with D3.js ('inferno', 'magma', 'turbo', 'rainbow', 'sinebow', 'warm', 'cool', 'cubehelix-default', 'category-10', 'observable-10', 'tableau-10')
|
||||
- Snapshot Markdown improvements
|
||||
- Add `MarkdownExtensionManager` (`PluginContext.managers.markdownExtensions`)
|
||||
- Support custom markdown commands to control the plugin via the `[link](!command)` pattern
|
||||
- Support rendering custom elements via the `` pattern
|
||||
- Support tables
|
||||
- Support loading images from MVSX files
|
||||
- Indicate external links with ⤴
|
||||
- Avoid calculating rings for coarse-grained structures
|
||||
- Fix isosurface compute shader normals when transformation matrix is applied to volume
|
||||
- Breaking: `PluginContext.initViewer/initContainer/mount` are now async and have been renamed to include `Async` postfix
|
||||
|
||||
@@ -94,7 +94,7 @@ The extension uses several transformations to process and visualize tunnel data:
|
||||
To help users understand how to use these transformations in practice, include detailed examples:
|
||||
|
||||
### Visualizing Multiple Tunnels
|
||||
This example ([runVisualizeTunnels](../../../src/extensions/sb-ncbr/tunnels/examples.ts#L19)) demonstrates how to visualize multiple tunnels from a fetched dataset.
|
||||
This example (see `src/extensions/sb-ncbr/tunnels/examples.ts#L19`) demonstrates how to visualize multiple tunnels from a fetched dataset.
|
||||
```typescript
|
||||
update.toRoot()
|
||||
.apply(TunnelsFromRawData, { data: tunnels })
|
||||
@@ -104,7 +104,7 @@ update.toRoot()
|
||||
```
|
||||
|
||||
### Visualizing a Single Tunnel
|
||||
This example ([runVisualizeTunnel](../../../src/extensions/sb-ncbr/tunnels/examples.ts#L46)) shows how to visualize a single tunnel.
|
||||
This example (see `src/extensions/sb-ncbr/tunnels/examples.ts#L46`) shows how to visualize a single tunnel.
|
||||
```typescript
|
||||
update.toRoot()
|
||||
.apply(TunnelFromRawData, {
|
||||
|
||||
94
docs/docs/plugin/managers/markdown-extensions.md
Normal file
94
docs/docs/plugin/managers/markdown-extensions.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Markdown Extension Manager
|
||||
|
||||
The `markdownExtensions` manager in `PluginContext.manager` allows customizing
|
||||
the `Markdown` React component to enable executing commands and rendering custom content.
|
||||
|
||||
The main use case of this is enriching [MolViewSpec](`https://molstar.org/mol-view-spec`) support.
|
||||
|
||||
## API
|
||||
|
||||
- `PluginContext.manager.markdownExtensions.register*` functions can be used to register extensions and state/data resolvers to make the the manager work with plugin extension
|
||||
- `PluginContext.manager.markdownExtensions.remove*` can be used to dynamically remove the above
|
||||
|
||||
## Commands
|
||||
|
||||
Extends Markdown Hyperlink syntax to support expressions of the form `[title](!c1=v1&c2=v2&...)` into an executable command. The command can be executed either on click, mouse enter, or mouse leave.
|
||||
|
||||
### Built-in Commands
|
||||
|
||||
- `center-camera` - Centers the camera
|
||||
- `apply-snapshot=key` - Loads snapshots with the provided key
|
||||
- `focus-refs=ref1,ref2,...` - On click, focuses nodes with the provided refs
|
||||
- `highlight-refs=ref1,ref2,...` - On mouse over, highlights the provided refs
|
||||
|
||||
## Custom Content
|
||||
|
||||
Extends Markdown Image syntax to support expressions of the form `` to render custom elements instead.
|
||||
|
||||
### Built-in Custom Content
|
||||
- `color-swatch=color` - Renders a box with the provided color
|
||||
- Color palettes:
|
||||
- `color-palette-name=name` - Renders a gradient with the provivided named color palette (see `mol-util/color/lists.ts` for supported color schemes)
|
||||
- `color-palette-colors=color1,color2` - Renders a gradient with the provided colors
|
||||
- `color-palette-width=CCS-value` - Specifies the width of the element, defaults to `150px`
|
||||
- `color-palette-height=CCS-value` - Specified the height of the element, defaults to `0.5em`
|
||||
- `color-palette-discrete` - Renders discrete color list instead of interpolating
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
```markdown
|
||||
### Highlight/Focus:
|
||||
-  [polymer](!highlight-refs=polymer&focus-refs=polymer)
|
||||
-  [ligand](!highlight-refs=ligand&focus-refs=ligand)
|
||||
- [both](!highlight-refs=polymer,ligand&focus-refs=polymer,ligand)
|
||||
|
||||
### Color Palettes
|
||||
|name|visual|
|
||||
|---:|---|
|
||||
|viridis||
|
||||
|rainbow (discrete)||
|
||||
|custom|)|
|
||||
|
||||
### Camera controls
|
||||
- [center](!center-camera)
|
||||
|
||||
### Image embedded in MVSX file
|
||||

|
||||
```
|
||||
|
||||
This works with the MolViewSpec state built by:
|
||||
|
||||
```py
|
||||
import molviewspec as mvs
|
||||
|
||||
builder = mvs.create_builder()
|
||||
|
||||
assets = {
|
||||
"1cbs.cif": "https://files.wwpdb.org/download/1cbs.cif",
|
||||
"logo.png": "https://molstar.org/img/molstar-logo.png",
|
||||
}
|
||||
|
||||
model = (
|
||||
builder.download(url="1cbs.cif")
|
||||
.parse(format="mmcif")
|
||||
.model_structure()
|
||||
)
|
||||
(
|
||||
model.component(selector="polymer")
|
||||
.representation(ref="polymer")
|
||||
.color(color="blue")
|
||||
)
|
||||
(
|
||||
model.component(selector="ligand")
|
||||
.representation(ref="ligand")
|
||||
.color(color="red")
|
||||
)
|
||||
|
||||
mvsx = mvs.MVSX(
|
||||
data=builder.get_state(
|
||||
description="""...""" # inline the code above
|
||||
),
|
||||
assets=assets
|
||||
)
|
||||
```
|
||||
@@ -38,6 +38,8 @@ nav:
|
||||
- Data State: 'plugin/data-state.md'
|
||||
- File Formats: 'plugin/file-formats.md'
|
||||
- CIF Schemas: 'plugin/cif-schemas.md'
|
||||
- Managers:
|
||||
- Markdown Extensions: 'plugin/managers/markdown-extensions.md'
|
||||
- State Transforms:
|
||||
- Custom Trajectory: 'plugin/transforms/custom-trajectory.md'
|
||||
- Custom Conformation: 'plugin/transforms/custom-conformation.md'
|
||||
@@ -59,5 +61,5 @@ nav:
|
||||
- Interactions: 'extensions/interactions.md'
|
||||
- Misc:
|
||||
- Interesting PDB entries: misc/interesting-pdb-entries.md
|
||||
- Exporting component data: exporting-components.md
|
||||
- Exporting component data: misc/exporting-components.md
|
||||
repo_url: https://github.com/molstar/docs
|
||||
|
||||
359
package-lock.json
generated
359
package-lock.json
generated
@@ -26,6 +26,7 @@
|
||||
"io-ts": "^2.2.22",
|
||||
"node-fetch": "^2.7.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"swagger-ui-dist": "^5.24.0",
|
||||
"tslib": "^2.8.1",
|
||||
@@ -4043,6 +4044,16 @@
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ccount": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
||||
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -8222,6 +8233,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/longest-streak": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
|
||||
"integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -8343,6 +8364,16 @@
|
||||
"tmpl": "1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-table": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
|
||||
"integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -8351,6 +8382,34 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-find-and-replace": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
|
||||
"integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"unist-util-is": "^6.0.0",
|
||||
"unist-util-visit-parents": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
|
||||
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-from-markdown": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz",
|
||||
@@ -8374,6 +8433,121 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
|
||||
"integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-gfm-autolink-literal": "^2.0.0",
|
||||
"mdast-util-gfm-footnote": "^2.0.0",
|
||||
"mdast-util-gfm-strikethrough": "^2.0.0",
|
||||
"mdast-util-gfm-table": "^2.0.0",
|
||||
"mdast-util-gfm-task-list-item": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-autolink-literal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
|
||||
"integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"ccount": "^2.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-find-and-replace": "^3.0.0",
|
||||
"micromark-util-character": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-footnote": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
|
||||
"integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.1.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0",
|
||||
"micromark-util-normalize-identifier": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-strikethrough": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
|
||||
"integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-table": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
|
||||
"integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"markdown-table": "^3.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-task-list-item": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
|
||||
"integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-phrasing": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
|
||||
"integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"unist-util-is": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-hast": {
|
||||
"version": "13.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.0.2.tgz",
|
||||
@@ -8393,6 +8567,27 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-markdown": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
|
||||
"integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"longest-streak": "^3.0.0",
|
||||
"mdast-util-phrasing": "^4.0.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"micromark-util-classify-character": "^2.0.0",
|
||||
"micromark-util-decode-string": "^2.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"zwitch": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-string": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
|
||||
@@ -8507,6 +8702,127 @@
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
|
||||
"integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-extension-gfm-autolink-literal": "^2.0.0",
|
||||
"micromark-extension-gfm-footnote": "^2.0.0",
|
||||
"micromark-extension-gfm-strikethrough": "^2.0.0",
|
||||
"micromark-extension-gfm-table": "^2.0.0",
|
||||
"micromark-extension-gfm-tagfilter": "^2.0.0",
|
||||
"micromark-extension-gfm-task-list-item": "^2.0.0",
|
||||
"micromark-util-combine-extensions": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-autolink-literal": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
|
||||
"integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-footnote": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
|
||||
"integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-core-commonmark": "^2.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-normalize-identifier": "^2.0.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-strikethrough": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
|
||||
"integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-util-chunked": "^2.0.0",
|
||||
"micromark-util-classify-character": "^2.0.0",
|
||||
"micromark-util-resolve-all": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-table": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
|
||||
"integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-tagfilter": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
|
||||
"integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-task-list-item": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
|
||||
"integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-destination": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz",
|
||||
@@ -10331,6 +10647,24 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-gfm": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
|
||||
"integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-gfm": "^3.0.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"unified": "^11.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-parse": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
|
||||
@@ -10362,6 +10696,21 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-stringify": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
|
||||
"integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0",
|
||||
"unified": "^11.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -13247,6 +13596,16 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zwitch": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||
"integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
"io-ts": "^2.2.22",
|
||||
"node-fetch": "^2.7.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"swagger-ui-dist": "^5.24.0",
|
||||
"tslib": "^2.8.1",
|
||||
|
||||
@@ -8,13 +8,12 @@ import { BehaviorSubject, distinctUntilChanged, map } from 'rxjs';
|
||||
import { PluginComponent } from '../../../mol-plugin-state/component';
|
||||
import { getMVSStoriesContext, MVSStoriesContext } from '../context';
|
||||
import { MVSStoriesViewerModel } from './viewer';
|
||||
import Markdown from 'react-markdown';
|
||||
import { useBehavior } from '../../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { PluginStateSnapshotManager } from '../../../mol-plugin-state/manager/snapshots';
|
||||
import { MarkdownAnchor } from '../../../mol-plugin-ui/controls';
|
||||
import { PluginReactContext } from '../../../mol-plugin-ui/base';
|
||||
import { CSSProperties } from 'react';
|
||||
import { Markdown } from '../../../mol-plugin-ui/controls/markdown';
|
||||
|
||||
export class MVSStoriesSnapshotMarkdownModel extends PluginComponent {
|
||||
readonly context: MVSStoriesContext;
|
||||
@@ -99,7 +98,7 @@ export function MVSStoriesSnapshotMarkdownUI({ model }: { model: MVSStoriesSnaps
|
||||
<div style={{ flexGrow: 1, overflow: 'hidden', overflowY: 'auto', position: 'relative' }}>
|
||||
<div style={{ position: 'absolute', inset: 0 }}>
|
||||
<PluginReactContext.Provider value={model.viewer?.model.plugin as any}>
|
||||
<Markdown skipHtml components={{ a: MarkdownAnchor }}>{state.entry?.description ?? 'Description not available'}</Markdown>
|
||||
<Markdown>{state.entry?.description ?? 'Description not available'}</Markdown>
|
||||
</PluginReactContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use '../../mol-plugin-ui/skin/base/components/markdown.scss';
|
||||
|
||||
.mvs-stories-markdown-explanation {
|
||||
// Adapted from skeleton.css, The MIT License (MIT), Copyright (c) 2011-2014 Dave Gamache
|
||||
line-height: 1.4;
|
||||
@@ -161,6 +163,25 @@
|
||||
border-width: 0;
|
||||
border-top: 1px solid #E1E1E1;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid #E1E1E1;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #E1E1E1;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation:portrait) {
|
||||
|
||||
@@ -12,9 +12,10 @@ import { LociLabelProvider } from '../../mol-plugin-state/manager/loci-label';
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StructureRepresentationProvider } from '../../mol-repr/structure/representation';
|
||||
import { StateAction } from '../../mol-state';
|
||||
import { StateAction, StateObjectCell, StateTree } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
import { ColorTheme } from '../../mol-theme/color';
|
||||
import { fileToDataUri } from '../../mol-util/file';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { MVSAnnotationColorThemeProvider } from './components/annotation-color-theme';
|
||||
import { MVSAnnotationLabelRepresentationProvider } from './components/annotation-label/representation';
|
||||
@@ -109,6 +110,39 @@ export const MolViewSpec = PluginBehavior.create<{ autoAttach: boolean }>({
|
||||
for (const action of this.registrables.actions ?? []) {
|
||||
this.ctx.state.data.actions.add(action);
|
||||
}
|
||||
|
||||
this.ctx.managers.markdownExtensions.registerRefResolver('mvs', (plugin, refs) => {
|
||||
const mvsRefs = new Set(refs.map(ref => `mvs-ref:${ref}`));
|
||||
return StateTree.doPreOrder(
|
||||
plugin.state.data.tree,
|
||||
plugin.state.data.tree.root,
|
||||
{ mvsRefs, plugin, cells: [] as StateObjectCell[] },
|
||||
(n, _, s) => {
|
||||
if (!n.tags) return;
|
||||
for (const tag of n.tags) {
|
||||
if (!s.mvsRefs.has(tag)) continue;
|
||||
const cell = s.plugin.state.data.cells.get(n.ref);
|
||||
if (cell) {
|
||||
s.cells.push(cell);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).cells;
|
||||
});
|
||||
|
||||
this.ctx.managers.markdownExtensions.registerUriResolver('mvs', (plugin, uri) => {
|
||||
const { assets } = plugin.managers.asset;
|
||||
const asset = assets.find(a => a.file.name === uri);
|
||||
if (!asset) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return fileToDataUri(asset.file);
|
||||
} catch (e) {
|
||||
console.error(`MVS: Failed to convert asset file to data URI for '${uri}'`, e);
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
update(p: { autoAttach: boolean }) {
|
||||
const updated = this.params.autoAttach !== p.autoAttach;
|
||||
@@ -146,6 +180,7 @@ export const MolViewSpec = PluginBehavior.create<{ autoAttach: boolean }>({
|
||||
for (const action of this.registrables.actions ?? []) {
|
||||
this.ctx.state.data.actions.remove(action);
|
||||
}
|
||||
this.ctx.managers.markdownExtensions.removeRefResolver('mvs');
|
||||
}
|
||||
},
|
||||
params: () => ({
|
||||
|
||||
@@ -9,6 +9,7 @@ import { hashString } from '../../../mol-data/util';
|
||||
import { StateObject } from '../../../mol-state';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ColorNames } from '../../../mol-util/color/names';
|
||||
import { decodeColor as _decodeColor } from '../../../mol-util/color/utils';
|
||||
|
||||
|
||||
/** Represents either the result or the reason of failure of an operation that might have failed */
|
||||
@@ -100,19 +101,7 @@ export type ElementOfSet<S> = S extends Set<infer T> ? T : never
|
||||
/** Convert `colorString` (either X11 color name like 'magenta' or hex code like '#ff00ff') to Color.
|
||||
* Return `undefined` if `colorString` cannot be converted. */
|
||||
export function decodeColor(colorString: string | undefined | null): Color | undefined {
|
||||
if (colorString === undefined || colorString === null) return undefined;
|
||||
let result: Color | undefined;
|
||||
if (HexColor.is(colorString)) {
|
||||
if (colorString.length === 4) {
|
||||
// convert short form to full form (#f0f -> #ff00ff)
|
||||
colorString = `#${colorString[1]}${colorString[1]}${colorString[2]}${colorString[2]}${colorString[3]}${colorString[3]}`;
|
||||
}
|
||||
result = Color.fromHexStyle(colorString);
|
||||
if (result !== undefined && !isNaN(result)) return result;
|
||||
}
|
||||
result = ColorNames[colorString.toLowerCase() as keyof typeof ColorNames];
|
||||
if (result !== undefined) return result;
|
||||
return undefined;
|
||||
return _decodeColor(colorString);
|
||||
}
|
||||
|
||||
/** Regular expression matching a hexadecimal color string, e.g. '#FF1100' or '#f10' */
|
||||
|
||||
248
src/mol-plugin-state/manager/markdown-extensions.ts
Normal file
248
src/mol-plugin-state/manager/markdown-extensions.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { getCellBoundingSphere } from '../../mol-plugin-state/manager/focus-camera/focus-object';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { StateObjectCell } from '../../mol-state';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
|
||||
export type MarkdownExtensionEvent = 'click' | 'mouse-enter' | 'mouse-leave';
|
||||
|
||||
export interface MarkdownExtension {
|
||||
name: string;
|
||||
execute?: (options: {
|
||||
event: MarkdownExtensionEvent,
|
||||
args: Record<string, string>,
|
||||
manager: MarkdownExtensionManager
|
||||
}) => void;
|
||||
reactRenderFn?: (options: {
|
||||
args: Record<string, string>,
|
||||
manager: MarkdownExtensionManager
|
||||
}) => any;
|
||||
}
|
||||
|
||||
export const BuiltInMarkdownExtension: MarkdownExtension[] = [
|
||||
{
|
||||
name: 'center-camera',
|
||||
execute: ({ event, args, manager }) => {
|
||||
if (event !== 'click') return;
|
||||
if ('center-camera' in args) {
|
||||
manager.plugin.managers.camera.reset();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'apply-snapshot',
|
||||
execute: ({ event, args, manager }) => {
|
||||
if (event !== 'click') return;
|
||||
const key = args['apply-snapshot'];
|
||||
if (!key) return;
|
||||
manager.plugin.managers.snapshot.applyKey(key);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'focus-refs',
|
||||
execute: ({ event, args, manager }) => {
|
||||
if (event !== 'click') return;
|
||||
const refs = parseArray(args['focus-refs']);
|
||||
if (!refs?.length) return;
|
||||
|
||||
const cells = manager.findCells(refs);
|
||||
if (!cells.length) return;
|
||||
|
||||
const reprs = findRepresentations(manager.plugin, cells);
|
||||
if (!reprs.length) return;
|
||||
|
||||
const spheres = reprs.map(c => getCellBoundingSphere(manager.plugin, c.transform.ref)).filter(s => !!s);
|
||||
if (!spheres.length) return;
|
||||
manager.plugin.managers.camera.focusSpheres(spheres, s => s, { extraRadius: 3 });
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'highlight-refs',
|
||||
execute: ({ event, args, manager }) => {
|
||||
const refs = parseArray(args['highlight-refs']);
|
||||
if (!refs?.length) return;
|
||||
|
||||
if (event === 'mouse-leave' && refs.length) {
|
||||
manager.plugin.managers.interactivity.lociHighlights.clearHighlights();
|
||||
return;
|
||||
} else if (event === 'mouse-enter') {
|
||||
const cells = manager.findCells(refs);
|
||||
for (const cell of findRepresentations(manager.plugin, cells)) {
|
||||
if (!cell.obj?.data) continue;
|
||||
const { repr } = cell.obj.data;
|
||||
for (const loci of repr.getAllLoci()) {
|
||||
manager.plugin.managers.interactivity.lociHighlights.highlight({ loci, repr }, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export class MarkdownExtensionManager {
|
||||
private extension: MarkdownExtension[] = [];
|
||||
private refResolvers: Record<string, (plugin: PluginContext, refs: string[]) => StateObjectCell[]> = {
|
||||
default: (plugin: PluginContext, refs: string[]) => refs
|
||||
.map(ref => plugin.state.data.cells.get(ref))
|
||||
.filter(c => !!c),
|
||||
};
|
||||
private uriResolvers: Record<string, (plugin: PluginContext, uri: string) => Promise<string> | string | undefined> = {};
|
||||
private argsParsers: [name: string, priority: number, parser: (input: string | undefined) => Record<string, string> | undefined][] = [
|
||||
['default', 100, defaultParseMarkdownCommandArgs],
|
||||
];
|
||||
|
||||
/**
|
||||
* Default parser has priority 100, parsers with higher priority
|
||||
* will be called first.
|
||||
*/
|
||||
registerArgsParser(name: string, priority: number, parser: (input: string | undefined) => Record<string, string> | undefined) {
|
||||
this.removeArgsParser(name);
|
||||
this.argsParsers.push([name, priority, parser]);
|
||||
this.argsParsers.sort((a, b) => b[1] - a[1]); // Sort by priority, higher first
|
||||
}
|
||||
|
||||
removeArgsParser(name: string) {
|
||||
const idx = this.argsParsers.findIndex(p => p[0] === name);
|
||||
if (idx >= 0) {
|
||||
this.argsParsers.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
parseArgs(input: string | undefined): Record<string, string> | undefined {
|
||||
for (const [,, parser] of this.argsParsers) {
|
||||
const ret = parser(input);
|
||||
if (ret) return ret;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
registerRefResolver(name: string, resolver: (plugin: PluginContext, refs: string[]) => StateObjectCell[]) {
|
||||
this.refResolvers[name] = resolver;
|
||||
}
|
||||
|
||||
removeRefResolver(name: string) {
|
||||
delete this.refResolvers[name];
|
||||
}
|
||||
|
||||
registerUriResolver(name: string, resolver: (plugin: PluginContext, uri: string) => Promise<string> | string | undefined) {
|
||||
this.uriResolvers[name] = resolver;
|
||||
}
|
||||
|
||||
removeUriResolver(name: string) {
|
||||
delete this.uriResolvers[name];
|
||||
}
|
||||
|
||||
registerExtension(command: MarkdownExtension) {
|
||||
const existing = this.extension.findIndex(c => c.name === command.name);
|
||||
if (existing >= 0) {
|
||||
this.extension[existing] = command;
|
||||
} else {
|
||||
this.extension.push(command);
|
||||
}
|
||||
}
|
||||
|
||||
removeExtension(name: string) {
|
||||
const idx = this.extension.findIndex(c => c.name === name);
|
||||
if (idx >= 0) {
|
||||
this.extension.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private _tryRender(ext: MarkdownExtension, options: { args: Record<string, string>, manager: MarkdownExtensionManager }) {
|
||||
try {
|
||||
return ext.reactRenderFn?.(options);
|
||||
} catch (e) {
|
||||
console.error(`Failed to render markdown extension '${ext.name}'`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a markdown extension with the given arguments.
|
||||
* Default renderers are defined separately because we
|
||||
* don't want to include `react` outside of mol-plugin-ui.
|
||||
*/
|
||||
tryRender(args: Record<string, string>, defaultRenderers: MarkdownExtension[]): any {
|
||||
const options = { args, manager: this };
|
||||
for (const ext of this.extension) {
|
||||
const ret = this._tryRender(ext, options);
|
||||
if (ret) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
for (const ext of defaultRenderers) {
|
||||
const ret = this._tryRender(ext, options);
|
||||
if (ret) {
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
tryExecute(event: MarkdownExtensionEvent, args: Record<string, string>) {
|
||||
const options = { event, args, manager: this };
|
||||
for (const ext of this.extension) {
|
||||
try {
|
||||
ext.execute?.(options);
|
||||
} catch (e) {
|
||||
console.error(`Failed to execute markdown extension '${ext.name}'`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tryResolveUri(uri: string): Promise<string> | string | undefined {
|
||||
for (const resolver of Object.values(this.uriResolvers)) {
|
||||
const resolved = resolver(this.plugin, uri);
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findCells(refs: string[]): StateObjectCell[] {
|
||||
const added = new Set<string>();
|
||||
const ret: StateObjectCell[] = [];
|
||||
for (const resolver of Object.values(this.refResolvers)) {
|
||||
for (const cell of resolver(this.plugin, refs)) {
|
||||
if (cell && !added.has(cell.transform.ref)) {
|
||||
added.add(cell.transform.ref);
|
||||
ret.push(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
constructor(public plugin: PluginContext) {
|
||||
for (const command of BuiltInMarkdownExtension) {
|
||||
this.registerExtension(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseArray(input?: string): string[] {
|
||||
return input?.split(',').map(s => s.trim()).filter(s => s.length > 0) ?? [];
|
||||
}
|
||||
|
||||
function findRepresentations(plugin: PluginContext, cells: StateObjectCell[]): StateObjectCell[] {
|
||||
if (!cells.length) return [];
|
||||
return plugin.state.data.selectQ(q =>
|
||||
q.byValue(...cells).subtree().filter(c => PluginStateObject.isRepresentation3D(c.obj))
|
||||
);
|
||||
}
|
||||
|
||||
export function defaultParseMarkdownCommandArgs(input: string | undefined): Record<string, string> | undefined {
|
||||
if (!input?.startsWith('!')) return undefined;
|
||||
const entries = decodeURIComponent(input.substring(1))
|
||||
.split('&')
|
||||
.map(p => p.trim())
|
||||
.filter(p => p.length > 0)
|
||||
.map(p => p.split('=', 2).map(s => s.trim()));
|
||||
if (entries.length === 0) return undefined;
|
||||
return Object.fromEntries(entries);
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import Markdown from 'react-markdown';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { UpdateTrajectory } from '../mol-plugin-state/actions/structure';
|
||||
import { LociLabel } from '../mol-plugin-state/manager/loci-label';
|
||||
import { PluginStateObject } from '../mol-plugin-state/objects';
|
||||
@@ -26,6 +26,7 @@ import { VolumeStreamingControls, VolumeSourceControls } from './structure/volum
|
||||
import { PluginConfig } from '../mol-plugin/config';
|
||||
import { StructureSuperpositionControls } from './structure/superposition';
|
||||
import { StructureQuickStylesControls } from './structure/quick-styles';
|
||||
import { Markdown } from './controls/markdown';
|
||||
|
||||
export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: boolean, label: string }> {
|
||||
state = { show: false, label: '' };
|
||||
@@ -210,30 +211,11 @@ export function ViewportSnapshotDescription() {
|
||||
return <div id='snapinfo' className='msp-snapshot-description-wrapper'>
|
||||
{e.descriptionFormat === 'plaintext'
|
||||
&& e.description
|
||||
|| <Markdown skipHtml components={{ a: MarkdownAnchor }}>{e.description}</Markdown>
|
||||
|| <Markdown>{e.description}</Markdown>
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
|
||||
export function MarkdownAnchor({ href, children, element }: { href?: string, children?: any, element?: any }) {
|
||||
const plugin = React.useContext(PluginReactContext);
|
||||
|
||||
if (!href) return element;
|
||||
|
||||
if (href[0] === '#') {
|
||||
return <a href='#' onClick={(e) => {
|
||||
e.preventDefault();
|
||||
plugin.managers.snapshot.applyKey(href.substring(1));
|
||||
}}>{children}</a>;
|
||||
} else if (href) {
|
||||
return <a href={href} target='_blank' rel='noopener noreferrer'>{children}</a>;
|
||||
}
|
||||
|
||||
// TODO: consider adding more "commands", for example !reset-camera
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
export class AnimationViewportControls extends PluginUIComponent<{}, { isEmpty: boolean, isExpanded: boolean, isBusy: boolean, isAnimating: boolean, isPlaying: boolean }> {
|
||||
state = { isEmpty: true, isExpanded: false, isBusy: false, isAnimating: false, isPlaying: false };
|
||||
|
||||
@@ -306,7 +288,7 @@ export class LociLabels extends PluginUIComponent<{}, { labels: ReadonlyArray<Lo
|
||||
{this.state.labels.map((e, i) => {
|
||||
if (e.indexOf('\n') >= 0) {
|
||||
return <div className='msp-highlight-markdown-row' key={'' + i}>
|
||||
<Markdown skipHtml>{e}</Markdown>
|
||||
<ReactMarkdown skipHtml>{e}</ReactMarkdown>
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
||||
141
src/mol-plugin-ui/controls/markdown.tsx
Normal file
141
src/mol-plugin-ui/controls/markdown.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import ReactMarkdown, { Components } from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { PluginReactContext } from '../base';
|
||||
import { PluginUIContext } from '../context';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { MarkdownExtension } from '../../mol-plugin-state/manager/markdown-extensions';
|
||||
import { ColorLists } from '../../mol-util/color/lists';
|
||||
import { getColorGradient, getColorGradientBanded, parseColorList } from '../../mol-util/color/utils';
|
||||
|
||||
export function Markdown({ children, components }: { children?: string, components?: Components }) {
|
||||
return <div className='msp-markdown'>
|
||||
<ReactMarkdown
|
||||
skipHtml
|
||||
components={{ a: MarkdownAnchor, img: MarkdownImg, ...components }}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export function MarkdownImg({ src, element, alt }: { src?: string, element?: any, alt?: string }) {
|
||||
const plugin: PluginUIContext | undefined = useContext(PluginReactContext);
|
||||
|
||||
if (!src) return element;
|
||||
|
||||
warnMissingPlugin(plugin);
|
||||
const args = plugin?.managers.markdownExtensions.parseArgs(src);
|
||||
if (args) {
|
||||
const result = plugin?.managers.markdownExtensions.tryRender(args, DefaultMarkdownExtensionRenderers);
|
||||
return result ?? element;
|
||||
} else {
|
||||
const data = plugin?.managers.markdownExtensions.tryResolveUri(src);
|
||||
if (typeof (data as Promise<string>)?.then === 'function') {
|
||||
return <LazyStaticImg alt={alt} data={data as Promise<string>} />;
|
||||
} else if (typeof data === 'string' && data) {
|
||||
return <img src={data} alt={alt} />;
|
||||
}
|
||||
}
|
||||
|
||||
return <img src={src} alt={alt} />;
|
||||
}
|
||||
|
||||
function LazyStaticImg({ alt, data }: { alt?: string, data: Promise<string> }) {
|
||||
const [src, setSrc] = useState<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
data.then(d => {
|
||||
if (mounted) setSrc(d);
|
||||
}).catch(e => {
|
||||
console.error('Failed to load static image', e);
|
||||
if (mounted) setSrc(undefined);
|
||||
});
|
||||
return () => { mounted = false; };
|
||||
}, [data]);
|
||||
if (!src) return null;
|
||||
return <img src={src} alt={alt} />;
|
||||
}
|
||||
|
||||
export const DefaultMarkdownExtensionRenderers: MarkdownExtension[] = [
|
||||
{
|
||||
name: 'color-swatch',
|
||||
reactRenderFn: ({ args }) => {
|
||||
const color = args['color-swatch'];
|
||||
if (!color) return null;
|
||||
return <span style={{ display: 'inline-block', width: '0.75em', height: '0.75em', backgroundColor: color, borderRadius: '25%' }}/>;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'color-palette',
|
||||
reactRenderFn: ({ args }) => {
|
||||
const name = args['color-palette-name'];
|
||||
const colors = args['color-palette-colors'];
|
||||
const minWidth = args['color-palette-width'] ?? '150px';
|
||||
const height = args['color-palette-height'] ?? '0.5em';
|
||||
const discrete = 'color-palette-discrete' in args;
|
||||
if (!name && !colors) return null;
|
||||
|
||||
const list = colors
|
||||
? parseColorList(colors)
|
||||
: ColorLists[name.toLowerCase() as keyof typeof ColorLists]?.list;
|
||||
|
||||
if (!list?.length) {
|
||||
console.warn(`Color palette could not be resolved.`, args);
|
||||
return null;
|
||||
}
|
||||
|
||||
return <span style={{
|
||||
display: 'inline-block',
|
||||
minWidth,
|
||||
height,
|
||||
background: (discrete ? getColorGradientBanded : getColorGradient)(list),
|
||||
borderRadius: '2px'
|
||||
}} />;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export function MarkdownAnchor({ href, children, element }: { href?: string, children?: any, element?: any }) {
|
||||
const plugin: PluginUIContext | undefined = useContext(PluginReactContext);
|
||||
|
||||
if (!href) return element;
|
||||
|
||||
warnMissingPlugin(plugin);
|
||||
const args = plugin?.managers.markdownExtensions.parseArgs(href);
|
||||
|
||||
if (args) {
|
||||
return <a href='#'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
plugin?.managers.markdownExtensions.tryExecute('click', args);
|
||||
}}
|
||||
onMouseEnter={() => plugin?.managers.markdownExtensions.tryExecute('mouse-enter', args)}
|
||||
onMouseLeave={() => plugin?.managers.markdownExtensions.tryExecute('mouse-leave', args)}
|
||||
>
|
||||
{children}
|
||||
</a>;
|
||||
} else if (href[0] === '#') {
|
||||
warnMissingPlugin(plugin);
|
||||
return <a href='#' onClick={(e) => {
|
||||
e.preventDefault();
|
||||
plugin?.managers.snapshot.applyKey(href.substring(1));
|
||||
}}>{children}</a>;
|
||||
} else if (href) {
|
||||
return <a href={href} target='_blank' rel='noopener noreferrer'>{children}⤴</a>;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
function warnMissingPlugin(plugin: PluginContext | undefined) {
|
||||
if (plugin) return;
|
||||
console.warn('Markdown component requires a PluginReactContext to be set.');
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import { ArrowDownwardSvg, ArrowDropDownSvg, ArrowRightSvg, ArrowUpwardSvg, Book
|
||||
import { legendFor } from './legend';
|
||||
import { LineGraphComponent } from './line-graph/line-graph-component';
|
||||
import { Slider, Slider2 } from './slider';
|
||||
import { getColorGradient, getColorGradientBanded } from '../../mol-util/color/utils';
|
||||
|
||||
export type ParameterControlsCategoryFilter = string | null | (string | null)[]
|
||||
|
||||
@@ -666,67 +667,9 @@ export class ColorControl extends SimpleParam<PD.Color> {
|
||||
}
|
||||
}
|
||||
|
||||
function colorEntryToStyle(e: ColorListEntry, includeOffset = false) {
|
||||
if (Array.isArray(e)) {
|
||||
if (includeOffset) return `${Color.toStyle(e[0])} ${(100 * e[1]).toFixed(2)}%`;
|
||||
return Color.toStyle(e[0]);
|
||||
}
|
||||
return Color.toStyle(e);
|
||||
}
|
||||
const colorGradientInterpolated = memoize1(getColorGradient);
|
||||
|
||||
const colorGradientInterpolated = memoize1((colors: ColorListEntry[]) => {
|
||||
if (colors.length === 0) return 'linear-gradient(to right, #000 0%, #000 100%)';
|
||||
|
||||
const hasOffsets = colors.every(c => Array.isArray(c));
|
||||
let styles;
|
||||
|
||||
if (hasOffsets) {
|
||||
const off = [...colors] as [Color, number][];
|
||||
off.sort((a, b) => a[1] - b[1]);
|
||||
styles = off.map(c => colorEntryToStyle(c, true));
|
||||
} else {
|
||||
styles = colors.map(c => colorEntryToStyle(c));
|
||||
}
|
||||
|
||||
return `linear-gradient(to right, ${styles.join(', ')})`;
|
||||
});
|
||||
|
||||
const colorGradientBanded = memoize1((colors: ColorListEntry[]) => {
|
||||
const n = colors.length;
|
||||
const styles: string[] = [];
|
||||
|
||||
const hasOffsets = colors.every(c => Array.isArray(c));
|
||||
if (hasOffsets) {
|
||||
const off = [...colors] as [Color, number][];
|
||||
// 0 colors present
|
||||
if (!off[0]) {
|
||||
return 'linear-gradient(to right, #000 0%, #000 100%)';
|
||||
}
|
||||
off.sort((a, b) => a[1] - b[1]);
|
||||
styles.push(`${Color.toStyle(off[0][0])} ${(100 * off[0][1]).toFixed(2)}%`);
|
||||
for (let i = 0, il = off.length - 1; i < il; ++i) {
|
||||
const [c0, o0] = off[i];
|
||||
const [c1, o1] = off[i + 1];
|
||||
const o = o0 + (o1 - o0) / 2;
|
||||
styles.push(
|
||||
`${Color.toStyle(c0)} ${(100 * o).toFixed(2)}%`,
|
||||
`${Color.toStyle(c1)} ${(100 * o).toFixed(2)}%`
|
||||
);
|
||||
}
|
||||
styles.push(`${Color.toStyle(off[off.length - 1][0])} ${(100 * off[off.length - 1][1]).toFixed(2)}%`);
|
||||
} else {
|
||||
styles.push(`${colorEntryToStyle(colors[0])} ${100 * (1 / n)}%`);
|
||||
for (let i = 1, il = n - 1; i < il; ++i) {
|
||||
styles.push(
|
||||
`${colorEntryToStyle(colors[i])} ${100 * (i / n)}%`,
|
||||
`${colorEntryToStyle(colors[i])} ${100 * ((i + 1) / n)}%`
|
||||
);
|
||||
}
|
||||
styles.push(`${colorEntryToStyle(colors[n - 1])} ${100 * ((n - 1) / n)}%`);
|
||||
}
|
||||
|
||||
return `linear-gradient(to right, ${styles.join(', ')})`;
|
||||
});
|
||||
const colorGradientBanded = memoize1(getColorGradientBanded);
|
||||
|
||||
function colorStripStyle(list: PD.ColorList['defaultValue'], right = '0'): React.CSSProperties {
|
||||
return {
|
||||
|
||||
25
src/mol-plugin-ui/skin/base/components/markdown.scss
Normal file
25
src/mol-plugin-ui/skin/base/components/markdown.scss
Normal file
@@ -0,0 +1,25 @@
|
||||
@use '../vars' as *;
|
||||
@use '../common' as *;
|
||||
|
||||
@mixin markdown {
|
||||
.msp-markdown {
|
||||
table {
|
||||
border: 1px solid $border-color;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid $border-color;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
@use './components/line-graph' as *;
|
||||
@use './components/log' as *;
|
||||
@use './components/slider' as *;
|
||||
@use './components/markdown' as *;
|
||||
@use './components/misc' as *;
|
||||
@use './components/tasks' as *;
|
||||
@use './components/viewport' as *;
|
||||
@@ -38,6 +39,7 @@
|
||||
@include line-graph;
|
||||
@include log;
|
||||
@include slider;
|
||||
@include markdown;
|
||||
@include misc;
|
||||
@include tasks;
|
||||
@include viewport;
|
||||
|
||||
@@ -30,6 +30,7 @@ import { StructureHierarchyRef } from '../mol-plugin-state/manager/structure/hie
|
||||
import { StructureMeasurementManager } from '../mol-plugin-state/manager/structure/measurement';
|
||||
import { StructureSelectionManager } from '../mol-plugin-state/manager/structure/selection';
|
||||
import { VolumeHierarchyManager } from '../mol-plugin-state/manager/volume/hierarchy';
|
||||
import { MarkdownExtensionManager } from '../mol-plugin-state/manager/markdown-extensions';
|
||||
import { LeftPanelTabName, PluginLayout } from './layout';
|
||||
import { Representation } from '../mol-repr/representation';
|
||||
import { StructureRepresentationRegistry } from '../mol-repr/structure/registry';
|
||||
@@ -190,6 +191,7 @@ export class PluginContext {
|
||||
toast: new PluginToastManager(this),
|
||||
asset: new AssetManager(),
|
||||
task: new TaskManager(),
|
||||
markdownExtensions: new MarkdownExtensionManager(this),
|
||||
dragAndDrop: new DragAndDropManager(this),
|
||||
} as const;
|
||||
|
||||
|
||||
144
src/mol-util/color/utils.ts
Normal file
144
src/mol-util/color/utils.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Color, ColorListEntry } from './color';
|
||||
import { ColorNames } from './names';
|
||||
|
||||
const hexColorRegex = /^#([0-9A-F]{3}){1,2}$/i;
|
||||
const rgbColorRegex = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i;
|
||||
|
||||
export function decodeColor(colorString: string | undefined | null): Color | undefined {
|
||||
if (colorString === undefined || colorString === null) return undefined;
|
||||
let result: Color | undefined;
|
||||
if (hexColorRegex.test(colorString)) {
|
||||
if (colorString.length === 4) {
|
||||
// convert short form to full form (#f0f -> #ff00ff)
|
||||
colorString = `#${colorString[1]}${colorString[1]}${colorString[2]}${colorString[2]}${colorString[3]}${colorString[3]}`;
|
||||
}
|
||||
result = Color.fromHexStyle(colorString);
|
||||
if (result !== undefined && !isNaN(result)) return result;
|
||||
}
|
||||
|
||||
result = ColorNames[colorString.toLowerCase() as keyof typeof ColorNames];
|
||||
if (result !== undefined) return result;
|
||||
|
||||
const rgbMatch = rgbColorRegex.exec(colorString);
|
||||
if (rgbMatch) {
|
||||
const r = parseInt(rgbMatch[1], 10);
|
||||
const g = parseInt(rgbMatch[2], 10);
|
||||
const b = parseInt(rgbMatch[3], 10);
|
||||
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
|
||||
return Color.fromRgb(r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getColorGradientBanded(colors: ColorListEntry[]) {
|
||||
const n = colors.length;
|
||||
const styles: string[] = [];
|
||||
|
||||
const hasOffsets = colors.every(c => Array.isArray(c));
|
||||
if (hasOffsets) {
|
||||
const off = [...colors] as [Color, number][];
|
||||
// 0 colors present
|
||||
if (!off[0]) {
|
||||
return 'linear-gradient(to right, #000 0%, #000 100%)';
|
||||
}
|
||||
off.sort((a, b) => a[1] - b[1]);
|
||||
styles.push(`${Color.toStyle(off[0][0])} ${(100 * off[0][1]).toFixed(2)}%`);
|
||||
for (let i = 0, il = off.length - 1; i < il; ++i) {
|
||||
const [c0, o0] = off[i];
|
||||
const [c1, o1] = off[i + 1];
|
||||
const o = o0 + (o1 - o0) / 2;
|
||||
styles.push(
|
||||
`${Color.toStyle(c0)} ${(100 * o).toFixed(2)}%`,
|
||||
`${Color.toStyle(c1)} ${(100 * o).toFixed(2)}%`
|
||||
);
|
||||
}
|
||||
styles.push(`${Color.toStyle(off[off.length - 1][0])} ${(100 * off[off.length - 1][1]).toFixed(2)}%`);
|
||||
} else {
|
||||
styles.push(`${colorEntryToStyle(colors[0])} ${100 * (1 / n)}%`);
|
||||
for (let i = 1, il = n - 1; i < il; ++i) {
|
||||
styles.push(
|
||||
`${colorEntryToStyle(colors[i])} ${100 * (i / n)}%`,
|
||||
`${colorEntryToStyle(colors[i])} ${100 * ((i + 1) / n)}%`
|
||||
);
|
||||
}
|
||||
styles.push(`${colorEntryToStyle(colors[n - 1])} ${100 * ((n - 1) / n)}%`);
|
||||
}
|
||||
|
||||
return `linear-gradient(to right, ${styles.join(', ')})`;
|
||||
}
|
||||
|
||||
export function getColorGradient(colors: ColorListEntry[]) {
|
||||
if (colors.length === 0) return 'linear-gradient(to right, #000 0%, #000 100%)';
|
||||
|
||||
const hasOffsets = colors.every(c => Array.isArray(c));
|
||||
let styles;
|
||||
|
||||
if (hasOffsets) {
|
||||
const off = [...colors] as [Color, number][];
|
||||
off.sort((a, b) => a[1] - b[1]);
|
||||
styles = off.map(c => colorEntryToStyle(c, true));
|
||||
} else {
|
||||
styles = colors.map(c => colorEntryToStyle(c));
|
||||
}
|
||||
|
||||
return `linear-gradient(to right, ${styles.join(', ')})`;
|
||||
}
|
||||
|
||||
function colorEntryToStyle(e: ColorListEntry, includeOffset = false) {
|
||||
if (Array.isArray(e)) {
|
||||
if (includeOffset) return `${Color.toStyle(e[0])} ${(100 * e[1]).toFixed(2)}%`;
|
||||
return Color.toStyle(e[0]);
|
||||
}
|
||||
return Color.toStyle(e);
|
||||
}
|
||||
|
||||
export function parseColorList(input: string, separator: RegExp = /,/): ColorListEntry[] {
|
||||
const ret: ColorListEntry[] = [];
|
||||
const trimmed = input.replace(/\s+/g, '');
|
||||
let tokenStart = 0;
|
||||
let bracketLevel = 0;
|
||||
for (let i = 0, il = trimmed.length; i < il; ++i) {
|
||||
const c = trimmed[i];
|
||||
if (c === '(') {
|
||||
bracketLevel++;
|
||||
continue;
|
||||
} else if (c === ')') {
|
||||
if (bracketLevel > 0) {
|
||||
bracketLevel--;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bracketLevel > 0) continue;
|
||||
|
||||
if (!separator.test(c)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const color = trimmed.substring(tokenStart, i);
|
||||
tokenStart = i + 1;
|
||||
const decoded = decodeColor(color);
|
||||
if (decoded !== undefined) {
|
||||
ret.push(decoded);
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenStart < trimmed.length) {
|
||||
const color = trimmed.substring(tokenStart);
|
||||
const decoded = decodeColor(color);
|
||||
console.log(color, decoded);
|
||||
if (decoded !== undefined) {
|
||||
ret.push(decoded);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
33
src/mol-util/file.ts
Normal file
33
src/mol-util/file.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
export async function fileToDataUri(file: File): Promise<string> {
|
||||
const filename = file.name.toLowerCase() || 'file';
|
||||
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp'].some(ext => filename.endsWith(`.${ext}`));
|
||||
|
||||
let type = 'application/octet-stream';
|
||||
if (isImage) {
|
||||
const ext = filename.split('.').pop()?.toLowerCase();
|
||||
switch (ext) {
|
||||
case 'jpg':
|
||||
type = 'image/jpeg';
|
||||
break;
|
||||
default:
|
||||
type = `image/${ext}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const bytes = await file.arrayBuffer();
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(new Blob([bytes], { type }));
|
||||
const data = await new Promise<string>((resolve, reject) => {
|
||||
reader.onload = () => resolve(reader.result as string);
|
||||
reader.onerror = () => reject(reader.error);
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
Reference in New Issue
Block a user