From d0f447b361fc484bb2b97ca4b9d2b7987b95a2d4 Mon Sep 17 00:00:00 2001 From: david <58231312+davseby@users.noreply.github.com> Date: Sun, 11 Oct 2026 14:13:28 +0300 Subject: [PATCH] add icon validation to mcp and assistant tools --- .github/workflows/qa-web.yml | 4 + server/core/internal/assistant/icon/icon.go | 155 ++++ .../core/internal/assistant/icon/icon_test.go | 156 ++++ .../core/internal/assistant/icon/icons.json | 695 ++++++++++++++++++ .../core/internal/assistant/markup/build.go | 13 +- .../internal/assistant/markup/build_test.go | 25 +- .../assistant/markup/testdata/all_blocks.json | 2 +- .../assistant/markup/testdata/all_blocks.xml | 2 +- server/core/internal/assistant/tools/block.go | 1 + .../core/internal/assistant/tools/document.go | 25 +- .../internal/assistant/tools/document_test.go | 28 +- .../tools/testdata/tool_schemas.golden | 2 +- web/app/utils/icon.test.ts | 10 + 13 files changed, 1096 insertions(+), 22 deletions(-) create mode 100644 server/core/internal/assistant/icon/icon.go create mode 100644 server/core/internal/assistant/icon/icon_test.go create mode 100644 server/core/internal/assistant/icon/icons.json diff --git a/.github/workflows/qa-web.yml b/.github/workflows/qa-web.yml index d0897b79..a9854e10 100644 --- a/.github/workflows/qa-web.yml +++ b/.github/workflows/qa-web.yml @@ -9,12 +9,16 @@ on: - main paths: - web/** + # the icon list test compares against core's copy. + - server/core/internal/assistant/icon/icons.json - .github/workflows/qa-web.yml pull_request: branches: - main paths: - web/** + # the icon list test compares against core's copy. + - server/core/internal/assistant/icon/icons.json - .github/workflows/qa-web.yml # a newer push supersedes the still-running workflow of the stale commit diff --git a/server/core/internal/assistant/icon/icon.go b/server/core/internal/assistant/icon/icon.go new file mode 100644 index 00000000..b4fd43b2 --- /dev/null +++ b/server/core/internal/assistant/icon/icon.go @@ -0,0 +1,155 @@ +// Package icon checks icon ids against the ones the editor's picker +// offers. +package icon + +import ( + "cmp" + _ "embed" + "encoding/json" + "fmt" + "regexp" + "slices" + "strings" + "unicode" +) + +const ( + // _maxIDLength is the longest icon id Validate looks at. Every + // offered id is far shorter, and scoring a long one is slow. + _maxIDLength = 128 + + // _maxSuggestions is the number of close icons an error names. + _maxSuggestions = 8 + + // _exactScore is what a query word scores against an equal word. + _exactScore = 2 + + // _partialScore is what a query word scores against a word that + // holds it or that it holds. + _partialScore = 1 + + // _minPartialWord is the shortest word that matches as part of + // another. Shorter ones, such as net or js, are inside too many names. + _minPartialWord = 4 +) + +// _iconsJSON is the picker's icon list, in the picker's order. The web +// test in web/app/utils/icon.test.ts keeps it equal to the list there. +// +//go:embed icons.json +var _iconsJSON []byte + +// _styleSuffix matches the style and variant suffixes an icon id ends +// with, such as -fill, -24-filled or -2. +var _styleSuffix = regexp.MustCompile(`(-(fill|line|filled|outline|solid|\d+))+$`) + +// entry is one icon the picker offers. +type entry struct { + // ID specifies the Iconify id, such as mingcute:rocket-fill. + ID string `json:"id"` + + // Name specifies the words the picker shows and searches. + Name string `json:"name"` +} + +// words returns the lowercase words of the icon's name and id. +func (e entry) words() []string { + return slices.Concat(words(e.Name), extractWords(e.ID)) +} + +// Validate returns nil when id is an icon the picker offers, or an +// error naming the offered icons whose names are closest to it. +func Validate(id string) error { + if len(id) > _maxIDLength { + return fmt.Errorf("icon is %d bytes, but an icon id is at most %d; use an id from the icon picker", len(id), _maxIDLength) + } + + var entries []entry + if err := json.Unmarshal(_iconsJSON, &entries); err != nil { + // NOCOV: the embedded file is valid, or every Validate test fails. + return fmt.Errorf("parsing the embedded icon list: %w", err) + } + + if slices.ContainsFunc(entries, func(e entry) bool { return e.ID == id }) { + return nil + } + + closest := suggest(entries, id) + if len(closest) == 0 { + return fmt.Errorf("icon %q is not available and no available icon has a similar name; try a plainer word, such as mingcute:document-2-fill", id) + } + + return fmt.Errorf("icon %q is not available; use one of %s", id, strings.Join(closest, ", ")) +} + +// suggest returns the ids of the icons whose words best match the words +// of id, best first, ties in list order. +func suggest(entries []entry, id string) []string { + query := extractWords(id) + + type scored struct { + id string + score int + } + + var hits []scored + + for _, e := range entries { + iconWords := e.words() + total := 0 + + for _, q := range query { + best := 0 + + for _, w := range iconWords { + best = max(best, wordScore(q, w)) + } + + total += best + } + + if total > 0 { + hits = append(hits, scored{e.ID, total}) + } + } + + slices.SortStableFunc(hits, func(a, b scored) int { return cmp.Compare(b.score, a.score) }) + + out := make([]string, 0, _maxSuggestions) + for _, h := range hits[:min(len(hits), _maxSuggestions)] { + out = append(out, h.id) + } + + return out +} + +// wordScore returns the score of one word against another: exact when +// equal, partial when one holds the other and both are long enough, and +// 0 otherwise. +func wordScore(a, b string) int { + switch { + case a == b: + return _exactScore + case len(a) >= _minPartialWord && len(b) >= _minPartialWord && (strings.Contains(a, b) || strings.Contains(b, a)): + return _partialScore + default: + return 0 + } +} + +// extractWords returns the lowercase words of an icon id without its +// collection and style suffix. +func extractWords(id string) []string { + if _, rest, ok := strings.Cut(id, ":"); ok { + id = rest + } + + return words(_styleSuffix.ReplaceAllString(strings.ToLower(id), "")) +} + +// words splits s into lowercase words of letters and digits. +func words(s string) []string { + return strings.FieldsFunc(strings.ToLower(s), func(r rune) bool { + return !unicode.IsLetter(r) && !unicode.IsDigit(r) + }) +} diff --git a/server/core/internal/assistant/icon/icon_test.go b/server/core/internal/assistant/icon/icon_test.go new file mode 100644 index 00000000..9610883a --- /dev/null +++ b/server/core/internal/assistant/icon/icon_test.go @@ -0,0 +1,156 @@ +package icon + +import ( + "errors" + "slices" + "strings" + "testing" + + "github.com/oxynote/oxynote/server/core/pkg/testutil" + "github.com/stretchr/testify/assert" + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} + +func Test_entry_words(t *testing.T) { + t.Parallel() + + e := entry{ID: "simple-icons:amazonwebservices", Name: "aws"} + + assert.Equal(t, []string{"aws", "amazonwebservices"}, e.words()) +} + +func Test_Validate(t *testing.T) { + t.Parallel() + + cc := map[string]struct { + ID string + Err error + }{ + "Offered icon": {ID: "mingcute:rocket-fill"}, + "Id over the length limit": { + ID: strings.Repeat("a-", 65), + Err: errors.New("icon is 130 bytes, but an icon id is at most 128; use an id from the icon picker"), + }, + "Offered icon from another collection": {ID: "simple-icons:kubernetes"}, + "Unknown icon with close ones": { + ID: "lucide:rocket", + Err: errors.New(`icon "lucide:rocket" is not available; use one of mingcute:rocket-fill, mingcute:rocket-2-fill`), + }, + "Unknown icon with nothing close": { + ID: "mingcute:deploy-fill", + Err: errors.New(`icon "mingcute:deploy-fill" is not available and no available icon has a similar name; try a plainer word, such as mingcute:document-2-fill`), + }, + } + + for cn, c := range cc { + t.Run(cn, func(t *testing.T) { + t.Parallel() + + testutil.AssertEqualError(t, c.Err, Validate(c.ID)) + }) + } +} + +func Test_suggest(t *testing.T) { + t.Parallel() + + entries := []entry{ + {ID: "mingcute:book-fill", Name: "book"}, + {ID: "mingcute:folder-open-fill", Name: "folder open"}, + {ID: "mingcute:book-open-fill", Name: "book open"}, + {ID: "devicon-plain:postgresql", Name: "postgresql"}, + {ID: "mingcute:cube-fill", Name: "cube"}, + } + + cc := map[string]struct { + Entries []entry + ID string + Result []string + }{ + "Exact words rank first and ties keep list order": { + Entries: entries, + ID: "lucide:book-open", + Result: []string{"mingcute:book-open-fill", "mingcute:book-fill", "mingcute:folder-open-fill"}, + }, + "Word inside a name": { + Entries: entries, + ID: "mingcute:postgres-fill", + Result: []string{"devicon-plain:postgresql"}, + }, + "Nothing close": { + Entries: entries, + ID: "mingcute:deploy-fill", + Result: []string{}, + }, + "Result stops at the cap": { + Entries: slices.Repeat([]entry{{ID: "x:book", Name: "book"}}, _maxSuggestions+2), + ID: "book", + Result: slices.Repeat([]string{"x:book"}, _maxSuggestions), + }, + } + + for cn, c := range cc { + t.Run(cn, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, c.Result, suggest(c.Entries, c.ID)) + }) + } +} + +func Test_wordScore(t *testing.T) { + t.Parallel() + + cc := map[string]struct { + A string + B string + Result int + }{ + "Equal words": {A: "c", B: "c", Result: 2}, + "One holds the other": {A: "postgres", B: "postgresql", Result: 1}, + "Other holds the one": {A: "postgresql", B: "postgres", Result: 1}, + "Short word inside": {A: "net", B: "kubernetes"}, + "Unrelated words": {A: "deploy", B: "rocket"}, + } + + for cn, c := range cc { + t.Run(cn, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, c.Result, wordScore(c.A, c.B)) + }) + } +} + +func Test_extractWords(t *testing.T) { + t.Parallel() + + cc := map[string]struct { + ID string + Result []string + }{ + "Collection and fill": {ID: "mingcute:shield-check-fill", Result: []string{"shield", "check"}}, + "Fluent size and style": {ID: "fluent:cloud-sync-24-filled", Result: []string{"cloud", "sync"}}, + "Variant number": {ID: "mingcute:rocket-2-fill", Result: []string{"rocket"}}, + "No collection": {ID: "Rocket", Result: []string{"rocket"}}, + "Underscores and outline": {ID: "x:file_code-outline", Result: []string{"file", "code"}}, + } + + for cn, c := range cc { + t.Run(cn, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, c.Result, extractWords(c.ID)) + }) + } +} + +func Test_words(t *testing.T) { + t.Parallel() + + assert.Equal(t, []string{"let", "s", "encrypt", "node", "js"}, words("Let's encrypt node.js")) +} diff --git a/server/core/internal/assistant/icon/icons.json b/server/core/internal/assistant/icon/icons.json new file mode 100644 index 00000000..edb42318 --- /dev/null +++ b/server/core/internal/assistant/icon/icons.json @@ -0,0 +1,695 @@ +[ + { "id": "mingcute:document-2-fill", "name": "document" }, + { "id": "mingcute:document-3-fill", "name": "document" }, + { "id": "mingcute:documents-fill", "name": "documents" }, + { "id": "mingcute:doc-fill", "name": "doc" }, + { "id": "mingcute:file-code-fill", "name": "file code" }, + { "id": "mingcute:file-certificate-fill", "name": "file certificate" }, + { "id": "mingcute:file-check-fill", "name": "file check" }, + { "id": "mingcute:file-info-fill", "name": "file info" }, + { "id": "mingcute:file-locked-fill", "name": "file locked" }, + { "id": "mingcute:file-search-fill", "name": "file search" }, + { "id": "mingcute:file-warning-fill", "name": "file warning" }, + { "id": "mingcute:file-zip-fill", "name": "file zip" }, + { "id": "mingcute:pdf-fill", "name": "pdf" }, + { "id": "mingcute:markdown-fill", "name": "markdown" }, + { "id": "mingcute:xls-fill", "name": "xls" }, + { "id": "mingcute:folder-fill", "name": "folder" }, + { "id": "mingcute:folder-open-fill", "name": "folder open" }, + { "id": "mingcute:folders-fill", "name": "folders" }, + { "id": "mingcute:folder-locked-fill", "name": "folder locked" }, + { "id": "mingcute:book-fill", "name": "book" }, + { "id": "mingcute:book-2-fill", "name": "book" }, + { "id": "mingcute:book-3-fill", "name": "book" }, + { "id": "mingcute:book-6-fill", "name": "book" }, + { "id": "mingcute:notebook-fill", "name": "notebook" }, + { "id": "mingcute:notebook-2-fill", "name": "notebook" }, + { "id": "mingcute:diary-fill", "name": "diary" }, + { "id": "mingcute:news-fill", "name": "news" }, + { "id": "mingcute:paper-fill", "name": "paper" }, + { "id": "fluent:script-24-filled", "name": "script" }, + { "id": "mingcute:clipboard-fill", "name": "clipboard" }, + { "id": "mingcute:list-check-fill", "name": "list check" }, + { "id": "mingcute:list-ordered-fill", "name": "list ordered" }, + { "id": "ph:kanban-fill", "name": "kanban" }, + { "id": "fluent:steps-24-filled", "name": "steps" }, + { "id": "mingcute:task-fill", "name": "task" }, + { "id": "mingcute:task-2-fill", "name": "task" }, + { "id": "mingcute:to-do-fill", "name": "to do" }, + { "id": "mingcute:report-fill", "name": "report" }, + { "id": "mingcute:report-forms-fill", "name": "report forms" }, + { "id": "mingcute:presentation-1-fill", "name": "presentation" }, + { "id": "mingcute:presentation-2-fill", "name": "presentation" }, + { "id": "mingcute:whiteboard-fill", "name": "whiteboard" }, + { "id": "mingcute:black-board-fill", "name": "black board" }, + { "id": "mingcute:mind-map-fill", "name": "mind map" }, + { "id": "mingcute:sitemap-fill", "name": "sitemap" }, + { "id": "mingcute:timeline-fill", "name": "timeline" }, + { "id": "mingcute:version-fill", "name": "version" }, + { "id": "mingcute:quill-pen-fill", "name": "quill pen" }, + { "id": "mingcute:pen-fill", "name": "pen" }, + { "id": "mingcute:pencil-fill", "name": "pencil" }, + { "id": "mingcute:edit-fill", "name": "edit" }, + { "id": "mingcute:mark-pen-fill", "name": "mark pen" }, + { "id": "mingcute:signature-fill", "name": "signature" }, + { "id": "mingcute:certificate-fill", "name": "certificate" }, + { "id": "mingcute:seal-fill", "name": "seal" }, + { "id": "mingcute:quote-left-fill", "name": "quote left" }, + { "id": "mingcute:translate-fill", "name": "translate" }, + { "id": "mingcute:pin-fill", "name": "pin" }, + { "id": "mingcute:inbox-fill", "name": "inbox" }, + { "id": "mingcute:archive-fill", "name": "archive" }, + { "id": "mingcute:package-fill", "name": "package" }, + { "id": "mingcute:package-2-fill", "name": "package" }, + { "id": "mingcute:box-2-fill", "name": "box" }, + { "id": "mingcute:box-3-fill", "name": "box" }, + { "id": "mingcute:question-fill", "name": "question" }, + { "id": "mingcute:information-fill", "name": "information" }, + { "id": "mingcute:bulb-fill", "name": "bulb" }, + { "id": "mingcute:brain-fill", "name": "brain" }, + { "id": "mingcute:thought-fill", "name": "thought" }, + { "id": "mingcute:mortarboard-fill", "name": "mortarboard" }, + { "id": "mingcute:school-fill", "name": "school" }, + { "id": "mingcute:telescope-fill", "name": "telescope" }, + { "id": "mingcute:microscope-fill", "name": "microscope" }, + { "id": "mingcute:flask-fill", "name": "flask" }, + { "id": "mingcute:test-tube-fill", "name": "test tube" }, + { "id": "mingcute:science-fill", "name": "science" }, + { "id": "mingcute:formula-fill", "name": "formula" }, + { "id": "ph:sigma-fill", "name": "sigma" }, + { "id": "ph:math-operations-fill", "name": "math operations" }, + { "id": "ri:calculator-fill", "name": "calculator" }, + { "id": "ri:atom-fill", "name": "atom" }, + { "id": "ri:dna-fill", "name": "dna" }, + { "id": "mingcute:omega-fill", "name": "omega" }, + { "id": "mingcute:asterisk-fill", "name": "asterisk" }, + { "id": "mingcute:code-fill", "name": "code" }, + { "id": "mingcute:braces-fill", "name": "braces" }, + { "id": "fluent:braces-variable-24-filled", "name": "variable" }, + { "id": "ph:function-fill", "name": "function" }, + { "id": "ph:binary-fill", "name": "binary" }, + { "id": "mingcute:brackets-fill", "name": "brackets" }, + { "id": "mingcute:brackets-angle-fill", "name": "brackets angle" }, + { "id": "mingcute:markup-fill", "name": "markup" }, + { "id": "mingcute:terminal-fill", "name": "terminal" }, + { "id": "mingcute:terminal-box-fill", "name": "terminal box" }, + { "id": "fluent:window-dev-tools-24-filled", "name": "dev tools" }, + { "id": "mingcute:command-fill", "name": "command" }, + { "id": "mingcute:hotkey-fill", "name": "hotkey" }, + { "id": "mingcute:keyboard-fill", "name": "keyboard" }, + { "id": "mingcute:bug-fill", "name": "bug" }, + { "id": "mingcute:git-branch-fill", "name": "git branch" }, + { "id": "mingcute:git-commit-fill", "name": "git commit" }, + { "id": "mingcute:git-compare-fill", "name": "git compare" }, + { "id": "mingcute:git-merge-fill", "name": "git merge" }, + { "id": "mingcute:git-pull-request-fill", "name": "git pull request" }, + { "id": "mingcute:git-pull-request-close-fill", "name": "git pull request close" }, + { "id": "ri:git-repository-fill", "name": "git repository" }, + { "id": "ri:git-repository-private-fill", "name": "git repository private" }, + { "id": "mingcute:plugin-fill", "name": "plugin" }, + { "id": "mingcute:plugin-2-fill", "name": "plugin" }, + { "id": "ph:plugs-connected-fill", "name": "plugs connected" }, + { "id": "mingcute:components-fill", "name": "components" }, + { "id": "mingcute:layers-fill", "name": "layers" }, + { "id": "ph:stack-fill", "name": "stack" }, + { "id": "mingcute:layer-fill", "name": "layer" }, + { "id": "mingcute:cube-fill", "name": "cube" }, + { "id": "mingcute:cube-3d-fill", "name": "cube 3d" }, + { "id": "mingcute:hexagon-fill", "name": "hexagon" }, + { "id": "mingcute:hexagons-fill", "name": "hexagons" }, + { "id": "mingcute:process-fill", "name": "process" }, + { "id": "fluent:pipeline-24-filled", "name": "pipeline" }, + { "id": "fluent:flowchart-24-filled", "name": "flowchart" }, + { "id": "ph:queue-fill", "name": "queue" }, + { "id": "ri:infinity-fill", "name": "infinity" }, + { "id": "mingcute:transformation-fill", "name": "transformation" }, + { "id": "mingcute:random-fill", "name": "random" }, + { "id": "mingcute:repeat-fill", "name": "repeat" }, + { "id": "mingcute:shuffle-fill", "name": "shuffle" }, + { "id": "mingcute:switch-fill", "name": "switch" }, + { "id": "mingcute:toggle-right-fill", "name": "toggle right" }, + { "id": "mingcute:cursor-fill", "name": "cursor" }, + { "id": "mingcute:mouse-fill", "name": "mouse" }, + { "id": "mingcute:inspect-fill", "name": "inspect" }, + { "id": "mingcute:search-fill", "name": "search" }, + { "id": "mingcute:filter-fill", "name": "filter" }, + { "id": "mingcute:filter-2-fill", "name": "filter" }, + { "id": "mingcute:sort-ascending-fill", "name": "sort ascending" }, + { "id": "mingcute:sort-descending-fill", "name": "sort descending" }, + { "id": "mingcute:target-fill", "name": "target" }, + { "id": "mingcute:aiming-fill", "name": "aiming" }, + { "id": "mingcute:magic-1-fill", "name": "magic" }, + { "id": "mingcute:sparkles-fill", "name": "sparkles" }, + { "id": "mingcute:ai-fill", "name": "ai" }, + { "id": "mingcute:robot-fill", "name": "robot" }, + { "id": "mingcute:webhook-fill", "name": "webhook" }, + { "id": "mingcute:mqtt-fill", "name": "mqtt" }, + { "id": "mingcute:rss-fill", "name": "rss" }, + { "id": "mingcute:web-fill", "name": "web" }, + { "id": "mingcute:world-fill", "name": "world" }, + { "id": "mingcute:earth-fill", "name": "earth" }, + { "id": "mingcute:globe-fill", "name": "globe" }, + { "id": "mingcute:link-fill", "name": "link" }, + { "id": "mingcute:unlink-fill", "name": "unlink" }, + { "id": "mingcute:external-link-fill", "name": "external link" }, + { "id": "mingcute:qrcode-fill", "name": "qrcode" }, + { "id": "mingcute:barcode-fill", "name": "barcode" }, + { "id": "mingcute:scan-fill", "name": "scan" }, + { "id": "mingcute:server-fill", "name": "server" }, + { "id": "mingcute:server-2-fill", "name": "server" }, + { "id": "ri:instance-fill", "name": "instance" }, + { "id": "ph:shipping-container-fill", "name": "container" }, + { "id": "mingcute:storage-fill", "name": "storage" }, + { "id": "ri:database-2-fill", "name": "database" }, + { "id": "ri:hard-drive-2-fill", "name": "hard drive" }, + { "id": "mingcute:drive-fill", "name": "drive" }, + { "id": "mingcute:cylinder-fill", "name": "cylinder" }, + { "id": "mingcute:cylinder-2-fill", "name": "cylinder" }, + { "id": "mingcute:safe-box-fill", "name": "safe box" }, + { "id": "mingcute:usb-fill", "name": "usb" }, + { "id": "mingcute:usb-flash-disk-fill", "name": "usb flash disk" }, + { "id": "mingcute:router-modem-fill", "name": "router modem" }, + { "id": "mingcute:base-station-fill", "name": "base station" }, + { "id": "ph:network-fill", "name": "network" }, + { "id": "ph:broadcast-fill", "name": "broadcast" }, + { "id": "mingcute:wifi-fill", "name": "wifi" }, + { "id": "mingcute:signal-fill", "name": "signal" }, + { "id": "mingcute:bluetooth-fill", "name": "bluetooth" }, + { "id": "mingcute:nfc-fill", "name": "nfc" }, + { "id": "mingcute:cloud-fill", "name": "cloud" }, + { "id": "mingcute:cloud-2-fill", "name": "cloud" }, + { "id": "mingcute:clouds-fill", "name": "clouds" }, + { "id": "mingcute:cloud-lightning-fill", "name": "cloud lightning" }, + { "id": "fluent:cloud-sync-24-filled", "name": "cloud sync" }, + { "id": "mingcute:monitor-fill", "name": "monitor" }, + { "id": "mingcute:display-fill", "name": "display" }, + { "id": "mingcute:computer-fill", "name": "computer" }, + { "id": "mingcute:laptop-fill", "name": "laptop" }, + { "id": "mingcute:imac-fill", "name": "imac" }, + { "id": "mingcute:device-fill", "name": "device" }, + { "id": "mingcute:chip-fill", "name": "chip" }, + { "id": "ph:cpu-fill", "name": "cpu" }, + { "id": "ph:memory-fill", "name": "memory" }, + { "id": "ri:ram-fill", "name": "ram" }, + { "id": "ph:circuitry-fill", "name": "circuitry" }, + { "id": "mingcute:cellphone-fill", "name": "cellphone" }, + { "id": "mingcute:pad-fill", "name": "pad" }, + { "id": "mingcute:watch-fill", "name": "watch" }, + { "id": "mingcute:tv-1-fill", "name": "tv" }, + { "id": "mingcute:projector-fill", "name": "projector" }, + { "id": "mingcute:print-fill", "name": "print" }, + { "id": "mingcute:remote-fill", "name": "remote" }, + { "id": "mingcute:power-fill", "name": "power" }, + { "id": "mingcute:flash-fill", "name": "flash" }, + { "id": "mingcute:lightning-fill", "name": "lightning" }, + { "id": "mingcute:battery-fill", "name": "battery" }, + { "id": "mingcute:battery-charging-fill", "name": "battery charging" }, + { "id": "mingcute:charging-pile-fill", "name": "charging pile" }, + { "id": "mingcute:solar-panel-fill", "name": "solar panel" }, + { "id": "mingcute:high-voltage-power-fill", "name": "high voltage power" }, + { "id": "mingcute:engine-fill", "name": "engine" }, + { "id": "mingcute:fan-fill", "name": "fan" }, + { "id": "mingcute:thermometer-fill", "name": "thermometer" }, + { "id": "mingcute:fire-fill", "name": "fire" }, + { "id": "mingcute:flame-fill", "name": "flame" }, + { "id": "mingcute:drop-fill", "name": "drop" }, + { "id": "mingcute:wind-fill", "name": "wind" }, + { "id": "mingcute:tornado-fill", "name": "tornado" }, + { "id": "mingcute:thunderstorm-fill", "name": "thunderstorm" }, + { "id": "mingcute:sun-fill", "name": "sun" }, + { "id": "mingcute:moon-fill", "name": "moon" }, + { "id": "mingcute:moon-stars-fill", "name": "moon stars" }, + { "id": "mingcute:planet-fill", "name": "planet" }, + { "id": "mingcute:rocket-fill", "name": "rocket" }, + { "id": "mingcute:rocket-2-fill", "name": "rocket" }, + { "id": "mingcute:ufo-fill", "name": "ufo" }, + { "id": "mingcute:drone-fill", "name": "drone" }, + { "id": "mingcute:radar-fill", "name": "radar" }, + { "id": "mingcute:radar-2-fill", "name": "radar" }, + { "id": "mingcute:compass-fill", "name": "compass" }, + { "id": "mingcute:navigation-fill", "name": "navigation" }, + { "id": "mingcute:map-fill", "name": "map" }, + { "id": "mingcute:map-pin-fill", "name": "map pin" }, + { "id": "mingcute:location-fill", "name": "location" }, + { "id": "mingcute:route-fill", "name": "route" }, + { "id": "mingcute:directions-fill", "name": "directions" }, + { "id": "mingcute:road-fill", "name": "road" }, + { "id": "mingcute:bridge-fill", "name": "bridge" }, + { "id": "mingcute:tunnel-fill", "name": "tunnel" }, + { "id": "mingcute:tower-fill", "name": "tower" }, + { "id": "mingcute:tv-tower-fill", "name": "tv tower" }, + { "id": "mingcute:lighthouse-fill", "name": "lighthouse" }, + { "id": "mingcute:anchor-fill", "name": "anchor" }, + { "id": "mingcute:ship-fill", "name": "ship" }, + { "id": "mingcute:sailboat-fill", "name": "sailboat" }, + { "id": "mingcute:truck-fill", "name": "truck" }, + { "id": "mingcute:train-fill", "name": "train" }, + { "id": "mingcute:bus-fill", "name": "bus" }, + { "id": "mingcute:car-fill", "name": "car" }, + { "id": "mingcute:airplane-fill", "name": "airplane" }, + { "id": "mingcute:flight-takeoff-fill", "name": "flight takeoff" }, + { "id": "mingcute:flight-land-fill", "name": "flight land" }, + { "id": "mingcute:parachute-fill", "name": "parachute" }, + { "id": "mingcute:lifebuoy-fill", "name": "lifebuoy" }, + { "id": "mingcute:traffic-cone-fill", "name": "traffic cone" }, + { "id": "mingcute:traffic-lights-fill", "name": "traffic lights" }, + { "id": "mingcute:home-1-fill", "name": "home" }, + { "id": "mingcute:house-fill", "name": "house" }, + { "id": "mingcute:door-fill", "name": "door" }, + { "id": "mingcute:enter-door-fill", "name": "enter door" }, + { "id": "mingcute:exit-door-fill", "name": "exit door" }, + { "id": "mingcute:building-1-fill", "name": "building" }, + { "id": "mingcute:building-2-fill", "name": "building" }, + { "id": "mingcute:factory-fill", "name": "factory" }, + { "id": "mingcute:bank-fill", "name": "bank" }, + { "id": "mingcute:government-fill", "name": "government" }, + { "id": "mingcute:store-fill", "name": "store" }, + { "id": "mingcute:inventory-fill", "name": "inventory" }, + { "id": "mingcute:trunk-fill", "name": "trunk" }, + { "id": "mingcute:tower-crane-fill", "name": "tower crane" }, + { "id": "mingcute:ladder-fill", "name": "ladder" }, + { "id": "mingcute:hammer-fill", "name": "hammer" }, + { "id": "mingcute:screwdriver-fill", "name": "screwdriver" }, + { "id": "mingcute:tool-fill", "name": "tool" }, + { "id": "ph:toolbox-fill", "name": "toolbox" }, + { "id": "mingcute:settings-1-fill", "name": "settings" }, + { "id": "mingcute:settings-3-fill", "name": "settings" }, + { "id": "mingcute:settings-6-fill", "name": "settings" }, + { "id": "mingcute:pickax-fill", "name": "pickax" }, + { "id": "mingcute:axe-fill", "name": "axe" }, + { "id": "mingcute:shovel-fill", "name": "shovel" }, + { "id": "mingcute:magnet-fill", "name": "magnet" }, + { "id": "mingcute:broom-fill", "name": "broom" }, + { "id": "mingcute:recycle-fill", "name": "recycle" }, + { "id": "mingcute:wastebasket-fill", "name": "wastebasket" }, + { "id": "mingcute:delete-2-fill", "name": "delete" }, + { "id": "mingcute:chart-bar-fill", "name": "chart bar" }, + { "id": "mingcute:chart-bar-2-fill", "name": "chart bar" }, + { "id": "mingcute:chart-line-fill", "name": "chart line" }, + { "id": "mingcute:chart-pie-fill", "name": "chart pie" }, + { "id": "mingcute:chart-pie-2-fill", "name": "chart pie" }, + { "id": "mingcute:chart-vertical-fill", "name": "chart vertical" }, + { "id": "mingcute:chart-horizontal-fill", "name": "chart horizontal" }, + { "id": "mingcute:chart-decrease-fill", "name": "chart decrease" }, + { "id": "fluent:data-scatter-24-filled", "name": "scatter chart" }, + { "id": "fluent:data-treemap-24-filled", "name": "treemap" }, + { "id": "ph:graph-fill", "name": "graph" }, + { "id": "mingcute:trending-up-fill", "name": "trending up" }, + { "id": "mingcute:trending-down-fill", "name": "trending down" }, + { "id": "mingcute:stock-fill", "name": "stock" }, + { "id": "mingcute:performance-fill", "name": "performance" }, + { "id": "mingcute:dashboard-fill", "name": "dashboard" }, + { "id": "mingcute:dashboard-2-fill", "name": "dashboard" }, + { "id": "mingcute:dashboard-3-fill", "name": "dashboard" }, + { "id": "mingcute:dashboard-4-fill", "name": "dashboard" }, + { "id": "ph:gauge-fill", "name": "gauge" }, + { "id": "mingcute:counter-fill", "name": "counter" }, + { "id": "mingcute:percentage-fill", "name": "percentage" }, + { "id": "mingcute:wave-fill", "name": "wave" }, + { "id": "mingcute:sound-line-fill", "name": "sound line" }, + { "id": "mingcute:heartbeat-fill", "name": "heartbeat" }, + { "id": "mingcute:heartbeat-2-fill", "name": "heartbeat" }, + { "id": "mingcute:table-fill", "name": "table" }, + { "id": "mingcute:table-2-fill", "name": "table" }, + { "id": "mingcute:grid-fill", "name": "grid" }, + { "id": "mingcute:layout-fill", "name": "layout" }, + { "id": "mingcute:balance-fill", "name": "balance" }, + { "id": "mingcute:scale-fill", "name": "scale" }, + { "id": "mingcute:ruler-fill", "name": "ruler" }, + { "id": "mingcute:pencil-ruler-fill", "name": "pencil ruler" }, + { "id": "mingcute:gradienter-fill", "name": "gradienter" }, + { "id": "mingcute:time-fill", "name": "time" }, + { "id": "mingcute:clock-fill", "name": "clock" }, + { "id": "mingcute:stopwatch-fill", "name": "stopwatch" }, + { "id": "mingcute:sandglass-fill", "name": "sandglass" }, + { "id": "mingcute:hours-fill", "name": "hours" }, + { "id": "mingcute:history-fill", "name": "history" }, + { "id": "mingcute:schedule-fill", "name": "schedule" }, + { "id": "mingcute:calendar-fill", "name": "calendar" }, + { "id": "mingcute:calendar-month-fill", "name": "calendar month" }, + { "id": "mingcute:calendar-week-fill", "name": "calendar week" }, + { "id": "mingcute:alarm-1-fill", "name": "alarm" }, + { "id": "mingcute:key-1-fill", "name": "key" }, + { "id": "mingcute:key-2-fill", "name": "key" }, + { "id": "mingcute:keyhole-fill", "name": "keyhole" }, + { "id": "fluent:password-24-filled", "name": "password" }, + { "id": "mingcute:lock-fill", "name": "lock" }, + { "id": "mingcute:unlock-fill", "name": "unlock" }, + { "id": "mingcute:shield-fill", "name": "shield" }, + { "id": "mingcute:shield-shape-fill", "name": "shield shape" }, + { "id": "mingcute:safe-shield-fill", "name": "safe shield" }, + { "id": "mingcute:safety-certificate-fill", "name": "safety certificate" }, + { "id": "mingcute:safe-lock-fill", "name": "safe lock" }, + { "id": "mingcute:safe-alert-fill", "name": "safe alert" }, + { "id": "mingcute:fingerprint-fill", "name": "fingerprint" }, + { "id": "mingcute:faceid-fill", "name": "faceid" }, + { "id": "mingcute:incognito-mode-fill", "name": "incognito mode" }, + { "id": "mingcute:eye-fill", "name": "eye" }, + { "id": "mingcute:eye-close-fill", "name": "eye close" }, + { "id": "mingcute:virus-fill", "name": "virus" }, + { "id": "mingcute:bomb-fill", "name": "bomb" }, + { "id": "mingcute:skull-fill", "name": "skull" }, + { "id": "mingcute:ghost-fill", "name": "ghost" }, + { "id": "mingcute:sword-fill", "name": "sword" }, + { "id": "mingcute:notification-fill", "name": "notification" }, + { "id": "mingcute:bell-ringing-fill", "name": "bell ringing" }, + { "id": "mingcute:announcement-fill", "name": "announcement" }, + { "id": "mingcute:horn-fill", "name": "horn" }, + { "id": "mingcute:flag-1-fill", "name": "flag" }, + { "id": "mingcute:flag-2-fill", "name": "flag" }, + { "id": "mingcute:flag-3-fill", "name": "flag" }, + { "id": "mingcute:flag-4-fill", "name": "flag" }, + { "id": "mingcute:check-circle-fill", "name": "check circle" }, + { "id": "mingcute:close-circle-fill", "name": "close circle" }, + { "id": "mingcute:forbid-circle-fill", "name": "forbid circle" }, + { "id": "mingcute:minus-circle-fill", "name": "minus circle" }, + { "id": "mingcute:add-circle-fill", "name": "add circle" }, + { "id": "mingcute:alert-fill", "name": "alert" }, + { "id": "mingcute:alert-octagon-fill", "name": "alert octagon" }, + { "id": "mingcute:alert-diamond-fill", "name": "alert diamond" }, + { "id": "mingcute:warning-fill", "name": "warning" }, + { "id": "mingcute:fault-fill", "name": "fault" }, + { "id": "mingcute:stethoscope-fill", "name": "stethoscope" }, + { "id": "mingcute:first-aid-kit-fill", "name": "first aid kit" }, + { "id": "mingcute:capsule-fill", "name": "capsule" }, + { "id": "mingcute:hospital-fill", "name": "hospital" }, + { "id": "mingcute:medal-fill", "name": "medal" }, + { "id": "mingcute:trophy-fill", "name": "trophy" }, + { "id": "mingcute:award-fill", "name": "award" }, + { "id": "mingcute:star-fill", "name": "star" }, + { "id": "mingcute:heart-fill", "name": "heart" }, + { "id": "mingcute:thumb-up-fill", "name": "thumb up" }, + { "id": "mingcute:thumb-down-fill", "name": "thumb down" }, + { "id": "mingcute:user-1-fill", "name": "user" }, + { "id": "mingcute:user-3-fill", "name": "user" }, + { "id": "mingcute:user-add-fill", "name": "user add" }, + { "id": "mingcute:user-setting-fill", "name": "user setting" }, + { "id": "mingcute:user-security-fill", "name": "user security" }, + { "id": "mingcute:user-lock-fill", "name": "user lock" }, + { "id": "mingcute:user-star-fill", "name": "user star" }, + { "id": "mingcute:user-search-fill", "name": "user search" }, + { "id": "mingcute:group-fill", "name": "group" }, + { "id": "mingcute:group-2-fill", "name": "group" }, + { "id": "mingcute:contacts-fill", "name": "contacts" }, + { "id": "mingcute:department-fill", "name": "department" }, + { "id": "mingcute:idcard-fill", "name": "idcard" }, + { "id": "mingcute:profile-fill", "name": "profile" }, + { "id": "mingcute:invite-fill", "name": "invite" }, + { "id": "mingcute:briefcase-fill", "name": "briefcase" }, + { "id": "mingcute:suitcase-fill", "name": "suitcase" }, + { "id": "mingcute:mail-fill", "name": "mail" }, + { "id": "mingcute:mail-send-fill", "name": "mail send" }, + { "id": "mingcute:mail-open-fill", "name": "mail open" }, + { "id": "mingcute:chat-1-fill", "name": "chat" }, + { "id": "mingcute:chat-2-fill", "name": "chat" }, + { "id": "mingcute:chat-4-fill", "name": "chat" }, + { "id": "mingcute:message-1-fill", "name": "message" }, + { "id": "mingcute:message-3-fill", "name": "message" }, + { "id": "mingcute:comment-fill", "name": "comment" }, + { "id": "mingcute:phone-fill", "name": "phone" }, + { "id": "mingcute:phone-call-fill", "name": "phone call" }, + { "id": "mingcute:video-fill", "name": "video" }, + { "id": "mingcute:video-camera-fill", "name": "video camera" }, + { "id": "mingcute:mic-fill", "name": "mic" }, + { "id": "mingcute:headphone-fill", "name": "headphone" }, + { "id": "mingcute:speaker-fill", "name": "speaker" }, + { "id": "mingcute:volume-fill", "name": "volume" }, + { "id": "mingcute:live-fill", "name": "live" }, + { "id": "mingcute:hands-clapping-fill", "name": "hands clapping" }, + { "id": "mingcute:wave-hand-fill", "name": "wave hand" }, + { "id": "mingcute:hand-heart-fill", "name": "hand heart" }, + { "id": "ph:handshake-fill", "name": "handshake" }, + { "id": "mingcute:celebrate-fill", "name": "celebrate" }, + { "id": "mingcute:firework-fill", "name": "firework" }, + { "id": "mingcute:gift-fill", "name": "gift" }, + { "id": "mingcute:cake-fill", "name": "cake" }, + { "id": "mingcute:teacup-fill", "name": "teacup" }, + { "id": "mingcute:game-1-fill", "name": "game" }, + { "id": "mingcute:chess-fill", "name": "chess" }, + { "id": "mingcute:leaf-fill", "name": "leaf" }, + { "id": "mingcute:tree-2-fill", "name": "tree" }, + { "id": "mingcute:wallet-fill", "name": "wallet" }, + { "id": "mingcute:coin-fill", "name": "coin" }, + { "id": "mingcute:cash-fill", "name": "cash" }, + { "id": "mingcute:bank-card-fill", "name": "bank card" }, + { "id": "mingcute:bill-fill", "name": "bill" }, + { "id": "mingcute:pig-money-fill", "name": "pig money" }, + { "id": "mingcute:currency-dollar-fill", "name": "currency dollar" }, + { "id": "mingcute:currency-euro-fill", "name": "currency euro" }, + { "id": "mingcute:receive-money-fill", "name": "receive money" }, + { "id": "mingcute:exchange-dollar-fill", "name": "exchange dollar" }, + { "id": "mingcute:sale-fill", "name": "sale" }, + { "id": "mingcute:coupon-fill", "name": "coupon" }, + { "id": "mingcute:ticket-fill", "name": "ticket" }, + { "id": "mingcute:auction-fill", "name": "auction" }, + { "id": "mingcute:github-fill", "name": "github" }, + { "id": "mingcute:git-lab-fill", "name": "git lab" }, + { "id": "mingcute:linux-fill", "name": "linux" }, + { "id": "mingcute:ubuntu-fill", "name": "ubuntu" }, + { "id": "mingcute:windows-fill", "name": "windows" }, + { "id": "mingcute:apple-fill", "name": "apple" }, + { "id": "mingcute:android-fill", "name": "android" }, + { "id": "mingcute:chrome-fill", "name": "chrome" }, + { "id": "mingcute:firefox-fill", "name": "firefox" }, + { "id": "mingcute:safari-fill", "name": "safari" }, + { "id": "mingcute:edge-fill", "name": "edge" }, + { "id": "mingcute:vscode-fill", "name": "vscode" }, + { "id": "mingcute:react-fill", "name": "react" }, + { "id": "mingcute:vue-fill", "name": "vue" }, + { "id": "mingcute:figma-fill", "name": "figma" }, + { "id": "mingcute:notion-fill", "name": "notion" }, + { "id": "mingcute:discord-fill", "name": "discord" }, + { "id": "mingcute:telegram-fill", "name": "telegram" }, + { "id": "mingcute:dropbox-fill", "name": "dropbox" }, + { "id": "mingcute:stripe-fill", "name": "stripe" }, + { "id": "mingcute:paypal-fill", "name": "paypal" }, + { "id": "mingcute:youtube-fill", "name": "youtube" }, + { "id": "mingcute:reddit-fill", "name": "reddit" }, + { "id": "mingcute:mastodon-fill", "name": "mastodon" }, + { "id": "mingcute:openai-fill", "name": "openai" }, + { "id": "mingcute:claude-fill", "name": "claude" }, + { "id": "mingcute:copilot-fill", "name": "copilot" }, + { "id": "mingcute:mcp-fill", "name": "mcp" }, + { "id": "mingcute:deepseek-fill", "name": "deepseek" }, + { "id": "mingcute:gemini-fill", "name": "gemini" }, + { "id": "mingcute:grok-fill", "name": "grok" }, + { "id": "mingcute:perplexity-fill", "name": "perplexity" }, + { "id": "mingcute:vibe-coding-fill", "name": "vibe coding" }, + { "id": "simple-icons:kubernetes", "name": "kubernetes" }, + { "id": "simple-icons:docker", "name": "docker" }, + { "id": "simple-icons:containerd", "name": "containerd" }, + { "id": "simple-icons:helm", "name": "helm" }, + { "id": "simple-icons:rancher", "name": "rancher" }, + { "id": "simple-icons:k3s", "name": "k3s" }, + { "id": "simple-icons:talos", "name": "talos" }, + { "id": "simple-icons:redhatopenshift", "name": "openshift" }, + { "id": "simple-icons:istio", "name": "istio" }, + { "id": "simple-icons:linkerd", "name": "linkerd" }, + { "id": "simple-icons:envoyproxy", "name": "envoy" }, + { "id": "simple-icons:argo", "name": "argo" }, + { "id": "simple-icons:openstack", "name": "openstack" }, + { "id": "simple-icons:proxmox", "name": "proxmox" }, + { "id": "file-icons:vmware", "name": "vmware" }, + { "id": "simple-icons:virtualbox", "name": "virtualbox" }, + { "id": "simple-icons:qemu", "name": "qemu" }, + { "id": "simple-icons:portainer", "name": "portainer" }, + { "id": "simple-icons:terraform", "name": "terraform" }, + { "id": "simple-icons:ansible", "name": "ansible" }, + { "id": "simple-icons:puppet", "name": "puppet" }, + { "id": "simple-icons:chef", "name": "chef" }, + { "id": "simple-icons:pulumi", "name": "pulumi" }, + { "id": "simple-icons:hashicorp", "name": "hashicorp" }, + { "id": "simple-icons:vault", "name": "vault" }, + { "id": "simple-icons:consul", "name": "consul" }, + { "id": "simple-icons:nomad", "name": "nomad" }, + { "id": "simple-icons:backstage", "name": "backstage" }, + { "id": "simple-icons:prometheus", "name": "prometheus" }, + { "id": "simple-icons:grafana", "name": "grafana" }, + { "id": "simple-icons:opentelemetry", "name": "opentelemetry" }, + { "id": "simple-icons:jaeger", "name": "jaeger" }, + { "id": "simple-icons:thanos", "name": "thanos" }, + { "id": "simple-icons:victoriametrics", "name": "victoriametrics" }, + { "id": "simple-icons:influxdb", "name": "influxdb" }, + { "id": "simple-icons:elasticsearch", "name": "elasticsearch" }, + { "id": "simple-icons:kibana", "name": "kibana" }, + { "id": "simple-icons:opensearch", "name": "opensearch" }, + { "id": "simple-icons:sentry", "name": "sentry" }, + { "id": "simple-icons:datadog", "name": "datadog" }, + { "id": "simple-icons:newrelic", "name": "new relic" }, + { "id": "simple-icons:pagerduty", "name": "pagerduty" }, + { "id": "simple-icons:uptimekuma", "name": "uptime kuma" }, + { "id": "devicon-plain:postgresql", "name": "postgresql" }, + { "id": "devicon-plain:mysql", "name": "mysql" }, + { "id": "simple-icons:mariadb", "name": "mariadb" }, + { "id": "simple-icons:sqlite", "name": "sqlite" }, + { "id": "simple-icons:mongodb", "name": "mongodb" }, + { "id": "simple-icons:redis", "name": "redis" }, + { "id": "simple-icons:clickhouse", "name": "clickhouse" }, + { "id": "simple-icons:cockroachlabs", "name": "cockroachdb" }, + { "id": "simple-icons:etcd", "name": "etcd" }, + { "id": "simple-icons:meilisearch", "name": "meilisearch" }, + { "id": "simple-icons:minio", "name": "minio" }, + { "id": "simple-icons:rabbitmq", "name": "rabbitmq" }, + { "id": "simple-icons:apachekafka", "name": "kafka" }, + { "id": "cib:cassandra", "name": "cassandra" }, + { "id": "cib:apache-spark", "name": "spark" }, + { "id": "simple-icons:snowflake", "name": "snowflake (data cloud)" }, + { "id": "simple-icons:databricks", "name": "databricks" }, + { "id": "simple-icons:supabase", "name": "supabase" }, + { "id": "ri:firebase-fill", "name": "firebase" }, + { "id": "simple-icons:prisma", "name": "prisma" }, + { "id": "simple-icons:metabase", "name": "metabase" }, + { "id": "simple-icons:posthog", "name": "posthog" }, + { "id": "simple-icons:googleanalytics", "name": "google analytics" }, + { "id": "simple-icons:nginx", "name": "nginx" }, + { "id": "file-icons:caddy", "name": "caddy" }, + { "id": "simple-icons:kong", "name": "kong" }, + { "id": "simple-icons:cloudflare", "name": "cloudflare" }, + { "id": "simple-icons:letsencrypt", "name": "let's encrypt" }, + { "id": "simple-icons:wireguard", "name": "wireguard" }, + { "id": "simple-icons:openvpn", "name": "openvpn" }, + { "id": "simple-icons:tailscale", "name": "tailscale" }, + { "id": "simple-icons:git", "name": "git" }, + { "id": "simple-icons:gitea", "name": "gitea" }, + { "id": "simple-icons:bitbucket", "name": "bitbucket" }, + { "id": "devicon-plain:jenkins", "name": "jenkins" }, + { "id": "simple-icons:circleci", "name": "circleci" }, + { "id": "simple-icons:dependabot", "name": "dependabot" }, + { "id": "simple-icons:codecov", "name": "codecov" }, + { "id": "simple-icons:snyk", "name": "snyk" }, + { "id": "simple-icons:trivy", "name": "trivy" }, + { "id": "simple-icons:owasp", "name": "owasp" }, + { "id": "simple-icons:amazonwebservices", "name": "aws" }, + { "id": "simple-icons:googlecloud", "name": "google cloud" }, + { "id": "simple-icons:googlebigquery", "name": "bigquery" }, + { "id": "simple-icons:microsoftazure", "name": "azure" }, + { "id": "simple-icons:azuredevops", "name": "azure devops" }, + { "id": "simple-icons:digitalocean", "name": "digitalocean" }, + { "id": "simple-icons:hetzner", "name": "hetzner" }, + { "id": "simple-icons:vercel", "name": "vercel" }, + { "id": "cib:netlify", "name": "netlify" }, + { "id": "simple-icons:heroku", "name": "heroku" }, + { "id": "simple-icons:flydotio", "name": "fly.io" }, + { "id": "simple-icons:railway", "name": "railway" }, + { "id": "simple-icons:fedora", "name": "fedora" }, + { "id": "mdi:debian", "name": "debian" }, + { "id": "simple-icons:archlinux", "name": "arch linux" }, + { "id": "simple-icons:alpinelinux", "name": "alpine linux" }, + { "id": "simple-icons:nixos", "name": "nixos" }, + { "id": "simple-icons:rockylinux", "name": "rocky linux" }, + { "id": "ri:centos-fill", "name": "centos" }, + { "id": "simple-icons:redhat", "name": "red hat" }, + { "id": "simple-icons:freebsd", "name": "freebsd" }, + { "id": "simple-icons:raspberrypi", "name": "raspberry pi" }, + { "id": "simple-icons:homeassistant", "name": "home assistant" }, + { "id": "simple-icons:eclipsemosquitto", "name": "mosquitto" }, + { "id": "simple-icons:zapier", "name": "zapier" }, + { "id": "devicon-plain:go", "name": "go" }, + { "id": "simple-icons:rust", "name": "rust" }, + { "id": "simple-icons:python", "name": "python" }, + { "id": "simple-icons:typescript", "name": "typescript" }, + { "id": "simple-icons:javascript", "name": "javascript" }, + { "id": "devicon-plain:nodejs", "name": "node.js" }, + { "id": "simple-icons:bun", "name": "bun" }, + { "id": "simple-icons:deno", "name": "deno" }, + { "id": "ri:php-fill", "name": "php" }, + { "id": "simple-icons:ruby", "name": "ruby" }, + { "id": "simple-icons:kotlin", "name": "kotlin" }, + { "id": "simple-icons:swift", "name": "swift" }, + { "id": "devicon-plain:dot-net", "name": ".net" }, + { "id": "simple-icons:csharp", "name": "c#" }, + { "id": "simple-icons:cplusplus", "name": "c++" }, + { "id": "simple-icons:c", "name": "c" }, + { "id": "simple-icons:zig", "name": "zig" }, + { "id": "simple-icons:elixir", "name": "elixir" }, + { "id": "simple-icons:erlang", "name": "erlang" }, + { "id": "simple-icons:haskell", "name": "haskell" }, + { "id": "simple-icons:scala", "name": "scala" }, + { "id": "simple-icons:lua", "name": "lua" }, + { "id": "devicon-plain:perl", "name": "perl" }, + { "id": "simple-icons:r", "name": "r" }, + { "id": "simple-icons:julia", "name": "julia" }, + { "id": "simple-icons:webassembly", "name": "webassembly" }, + { "id": "simple-icons:powershell", "name": "powershell" }, + { "id": "simple-icons:gnubash", "name": "bash" }, + { "id": "simple-icons:vuedotjs", "name": "vue.js" }, + { "id": "simple-icons:nuxt", "name": "nuxt" }, + { "id": "simple-icons:nextdotjs", "name": "next.js" }, + { "id": "simple-icons:svelte", "name": "svelte" }, + { "id": "simple-icons:angular", "name": "angular" }, + { "id": "simple-icons:astro", "name": "astro" }, + { "id": "simple-icons:hono", "name": "hono" }, + { "id": "simple-icons:nestjs", "name": "nestjs" }, + { "id": "simple-icons:fastapi", "name": "fastapi" }, + { "id": "simple-icons:django", "name": "django" }, + { "id": "simple-icons:flask", "name": "flask (python)" }, + { "id": "simple-icons:spring", "name": "spring" }, + { "id": "devicon-plain:laravel", "name": "laravel" }, + { "id": "devicon-plain:rails", "name": "ruby on rails" }, + { "id": "simple-icons:tailwindcss", "name": "tailwind css" }, + { "id": "simple-icons:vite", "name": "vite" }, + { "id": "simple-icons:webpack", "name": "webpack" }, + { "id": "simple-icons:eslint", "name": "eslint" }, + { "id": "simple-icons:prettier", "name": "prettier" }, + { "id": "simple-icons:vitest", "name": "vitest" }, + { "id": "simple-icons:jest", "name": "jest" }, + { "id": "simple-icons:playwright", "name": "playwright" }, + { "id": "simple-icons:cypress", "name": "cypress" }, + { "id": "simple-icons:storybook", "name": "storybook" }, + { "id": "simple-icons:graphql", "name": "graphql" }, + { "id": "simple-icons:openapiinitiative", "name": "openapi" }, + { "id": "simple-icons:swagger", "name": "swagger" }, + { "id": "simple-icons:postman", "name": "postman" }, + { "id": "simple-icons:npm", "name": "npm" }, + { "id": "simple-icons:pnpm", "name": "pnpm" }, + { "id": "simple-icons:yarn", "name": "yarn" }, + { "id": "simple-icons:d3dotjs", "name": "d3.js" }, + { "id": "simple-icons:mermaid", "name": "mermaid" }, + { "id": "simple-icons:excalidraw", "name": "excalidraw" }, + { "id": "simple-icons:diagramsdotnet", "name": "diagrams.net" }, + { "id": "simple-icons:json", "name": "json" }, + { "id": "simple-icons:yaml", "name": "yaml" }, + { "id": "simple-icons:anthropic", "name": "anthropic" }, + { "id": "simple-icons:huggingface", "name": "hugging face" }, + { "id": "simple-icons:pytorch", "name": "pytorch" }, + { "id": "simple-icons:tensorflow", "name": "tensorflow" }, + { "id": "simple-icons:ollama", "name": "ollama" }, + { "id": "simple-icons:langchain", "name": "langchain" }, + { "id": "simple-icons:mistralai", "name": "mistral" }, + { "id": "simple-icons:cursor", "name": "cursor (editor)" }, + { "id": "simple-icons:slack", "name": "slack" }, + { "id": "simple-icons:jira", "name": "jira" }, + { "id": "simple-icons:confluence", "name": "confluence" }, + { "id": "simple-icons:trello", "name": "trello" }, + { "id": "simple-icons:linear", "name": "linear" }, + { "id": "simple-icons:clickup", "name": "clickup" }, + { "id": "simple-icons:asana", "name": "asana" }, + { "id": "simple-icons:miro", "name": "miro" }, + { "id": "simple-icons:mattermost", "name": "mattermost" }, + { "id": "simple-icons:googlemeet", "name": "google meet" }, + { "id": "simple-icons:microsoftteams", "name": "microsoft teams" }, + { "id": "ph:matrix-logo-fill", "name": "matrix" }, + { "id": "simple-icons:obsidian", "name": "obsidian" }, + { "id": "simple-icons:readthedocs", "name": "read the docs" }, + { "id": "simple-icons:docusaurus", "name": "docusaurus" }, + { "id": "simple-icons:gitbook", "name": "gitbook" }, + { "id": "simple-icons:stackoverflow", "name": "stack overflow" }, + { "id": "simple-icons:jetbrains", "name": "jetbrains" }, + { "id": "simple-icons:neovim", "name": "neovim" }, + { "id": "simple-icons:vim", "name": "vim" }, + { "id": "simple-icons:tmux", "name": "tmux" }, + { "id": "simple-icons:1password", "name": "1password" }, + { "id": "simple-icons:bitwarden", "name": "bitwarden" }, + { "id": "simple-icons:keycloak", "name": "keycloak" }, + { "id": "simple-icons:auth0", "name": "auth0" }, + { "id": "simple-icons:okta", "name": "okta" }, + { "id": "simple-icons:authelia", "name": "authelia" }, + { "id": "simple-icons:authentik", "name": "authentik" }, + { "id": "simple-icons:wireshark", "name": "wireshark" }, + { "id": "simple-icons:metasploit", "name": "metasploit" }, + { "id": "simple-icons:burpsuite", "name": "burp suite" }, + { "id": "simple-icons:rclone", "name": "rclone" }, + { "id": "simple-icons:truenas", "name": "truenas" }, + { "id": "simple-icons:twilio", "name": "twilio" }, + { "id": "simple-icons:sendgrid", "name": "sendgrid" } +] diff --git a/server/core/internal/assistant/markup/build.go b/server/core/internal/assistant/markup/build.go index 61bf97f8..30335775 100644 --- a/server/core/internal/assistant/markup/build.go +++ b/server/core/internal/assistant/markup/build.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/oxynote/oxynote/server/core/internal/assistant/icon" "github.com/oxynote/oxynote/server/core/internal/document" "github.com/oxynote/oxynote/server/core/pkg/strutil" ) @@ -239,7 +240,17 @@ func (b *builder) buildContainer(n node, typ document.BlockNodeType, child strin switch typ { case document.BlockNodeCalloutBlock: - blk.Attrs[document.AttrIcon] = cmp.Or(n.attrs[document.AttrIcon], _defaultCalloutIcon) + id := cmp.Or(n.attrs[document.AttrIcon], _defaultCalloutIcon) + + // the default and a kept callout's own icon may be outside the + // picker's list. Both pass, since the caller did not pick them. + if id != _defaultCalloutIcon && id != stored.Attrs.Get(document.AttrIcon).String() { + if err := icon.Validate(id); err != nil { + return document.Block{}, fmt.Errorf("line %d: %w", n.line, err) + } + } + + blk.Attrs[document.AttrIcon] = id case document.BlockNodeOrderedList: if start, err := strconv.Atoi(n.attrs[document.AttrStart]); err == nil && start > 1 { blk.Attrs[document.AttrStart] = start diff --git a/server/core/internal/assistant/markup/build_test.go b/server/core/internal/assistant/markup/build_test.go index f0c54b5d..6fc4d10f 100644 --- a/server/core/internal/assistant/markup/build_test.go +++ b/server/core/internal/assistant/markup/build_test.go @@ -2,11 +2,13 @@ package markup import ( "errors" + "fmt" "maps" "regexp" "strings" "testing" + "github.com/oxynote/oxynote/server/core/internal/assistant/icon" "github.com/oxynote/oxynote/server/core/internal/document" "github.com/oxynote/oxynote/server/core/pkg/testutil" "github.com/stretchr/testify/assert" @@ -191,9 +193,26 @@ func Test_Build(t *testing.T) { "Id used twice": {Markup: `

x

y

`, Stored: root, Err: errors.New(`line 1: id "p2" is used twice; leave it out of the copy`)}, "Text outside an element": {Markup: "hello", Err: errors.New("line 1: text in the markup has to sit in an element such as

")}, "Text in a container": {Markup: "hi", Err: errors.New("line 1: text in has to sit in an element such as

")}, - "Tag in a heading": {Markup: "

a b

", Err: errors.New("line 1: a heading takes plain text, so is not allowed in it")}, - "Block inside text": {Markup: "

a


", Err: errors.New("line 1:
is a block; it cannot sit inside text")}, - "Link to a script": {Markup: `

x

`, Err: errors.New("line 1: a link cannot use javascript:; link to a web address instead")}, + "Callout icon the picker lacks": { + Markup: `

x

`, + Err: fmt.Errorf("line 1: %w", icon.Validate("lucide:rocket")), + }, + "Callout with the default icon": { + Markup: `

x

`, + Result: "\n

x

\n
\n", + }, + "Kept callout keeps an icon the picker lacks": { + Markup: `

x

`, + Stored: document.RootBlock{Content: []document.Block{{ + Type: document.BlockNodeCalloutBlock, + Attrs: document.Attributes{document.AttrUID: "c1", document.AttrIcon: "lucide:info"}, + Content: []document.Block{{Type: document.BlockNodeParagraph}}, + }}}, + Result: "\n

x

\n
\n", + }, + "Tag in a heading": {Markup: "

a b

", Err: errors.New("line 1: a heading takes plain text, so is not allowed in it")}, + "Block inside text": {Markup: "

a


", Err: errors.New("line 1:
is a block; it cannot sit inside text")}, + "Link to a script": {Markup: `

x

`, Err: errors.New("line 1: a link cannot use javascript:; link to a web address instead")}, // links a person stored may use any other scheme, or not even // parse, and a write that keeps them has to go through. "Links of other schemes stay": { diff --git a/server/core/internal/assistant/markup/testdata/all_blocks.json b/server/core/internal/assistant/markup/testdata/all_blocks.json index cfcf5776..40efacce 100644 --- a/server/core/internal/assistant/markup/testdata/all_blocks.json +++ b/server/core/internal/assistant/markup/testdata/all_blocks.json @@ -309,7 +309,7 @@ "type": "calloutBlock", "attrs": { "uid": "c1", - "icon": "lucide:info" + "icon": "mingcute:information-fill" }, "content": [ { diff --git a/server/core/internal/assistant/markup/testdata/all_blocks.xml b/server/core/internal/assistant/markup/testdata/all_blocks.xml index 4cd0236c..0812e383 100644 --- a/server/core/internal/assistant/markup/testdata/all_blocks.xml +++ b/server/core/internal/assistant/markup/testdata/all_blocks.xml @@ -17,7 +17,7 @@ Done Open - +

Callout