mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-17 10:59:20 +02:00
add icon validation to mcp and assistant tools
This commit is contained in:
parent
a4b374b0cf
commit
d0f447b361
4
.github/workflows/qa-web.yml
vendored
4
.github/workflows/qa-web.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
155
server/core/internal/assistant/icon/icon.go
Normal file
155
server/core/internal/assistant/icon/icon.go
Normal file
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
156
server/core/internal/assistant/icon/icon_test.go
Normal file
156
server/core/internal/assistant/icon/icon_test.go
Normal file
|
|
@ -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"))
|
||||
}
|
||||
695
server/core/internal/assistant/icon/icons.json
Normal file
695
server/core/internal/assistant/icon/icons.json
Normal file
|
|
@ -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" }
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: `<p id="p2">x</p><p id="p2">y</p>`, 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 <p>")},
|
||||
"Text in a container": {Markup: "<callout>hi</callout>", Err: errors.New("line 1: text in <callout> has to sit in an element such as <p>")},
|
||||
"Tag in a heading": {Markup: "<h2>a <b>b</b></h2>", Err: errors.New("line 1: a heading takes plain text, so <b> is not allowed in it")},
|
||||
"Block inside text": {Markup: "<p>a <hr/></p>", Err: errors.New("line 1: <hr> is a block; it cannot sit inside text")},
|
||||
"Link to a script": {Markup: `<p><a href=" Java Script:alert(1)">x</a></p>`, Err: errors.New("line 1: a link cannot use javascript:; link to a web address instead")},
|
||||
"Callout icon the picker lacks": {
|
||||
Markup: `<callout icon="lucide:rocket"><p>x</p></callout>`,
|
||||
Err: fmt.Errorf("line 1: %w", icon.Validate("lucide:rocket")),
|
||||
},
|
||||
"Callout with the default icon": {
|
||||
Markup: `<callout icon="lucide:text"><p>x</p></callout>`,
|
||||
Result: "<callout icon=\"lucide:text\">\n <p>x</p>\n</callout>\n",
|
||||
},
|
||||
"Kept callout keeps an icon the picker lacks": {
|
||||
Markup: `<callout id="c1" icon="lucide:info"><p>x</p></callout>`,
|
||||
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: "<callout icon=\"lucide:info\">\n <p>x</p>\n</callout>\n",
|
||||
},
|
||||
"Tag in a heading": {Markup: "<h2>a <b>b</b></h2>", Err: errors.New("line 1: a heading takes plain text, so <b> is not allowed in it")},
|
||||
"Block inside text": {Markup: "<p>a <hr/></p>", Err: errors.New("line 1: <hr> is a block; it cannot sit inside text")},
|
||||
"Link to a script": {Markup: `<p><a href=" Java Script:alert(1)">x</a></p>`, 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": {
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@
|
|||
"type": "calloutBlock",
|
||||
"attrs": {
|
||||
"uid": "c1",
|
||||
"icon": "lucide:info"
|
||||
"icon": "mingcute:information-fill"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<task id="ti1" checked="true">Done</task>
|
||||
<task id="ti2">Open</task>
|
||||
</tasks>
|
||||
<callout id="c1" icon="lucide:info">
|
||||
<callout id="c1" icon="mingcute:information-fill">
|
||||
<p id="c1p">Callout</p>
|
||||
<ul id="cl">
|
||||
<li id="cli">In callout</li>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ var _contentDescription = "The blocks as XML, one element per block. " +
|
|||
"<hr/>; <img src alt title width/>; <figma src width height/>; <metrics> of <metric>, which holds a JSON object of its attributes; " +
|
||||
"<split_doc inversed> holding <left>, an <h1> then <p>, lists or <callout> then any <params header> of <param name type>description</param>, and <right>, a titled <pre>, <metric> or <mermaid>. " +
|
||||
"Inline text takes <b>, <i>, <u>, <s>, <code> and <a href>; outside <pre>, <mermaid> and <metric>, whose content is raw, write & as & and < as <. " +
|
||||
"A callout's icon takes the ids create_document's icon does. " +
|
||||
"Mermaid ends a gantt task name or timeline period at its first colon, so write a colon inside one as #58;. " +
|
||||
"A metric's attributes: " + markup.MetricReference() + "."
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/guregu/null/v5"
|
||||
"github.com/oxynote/oxynote/server/core/internal/assistant/icon"
|
||||
"github.com/oxynote/oxynote/server/core/internal/assistant/markup"
|
||||
"github.com/oxynote/oxynote/server/core/internal/document"
|
||||
"github.com/oxynote/oxynote/server/core/internal/tag"
|
||||
|
|
@ -267,7 +268,7 @@ type createDocumentArgs struct {
|
|||
// Name is the new document's display name. Required.
|
||||
Name string `json:"name"`
|
||||
|
||||
// Icon is the lucide icon identifier. Empty falls back to the
|
||||
// Icon is an icon id the picker offers. Empty falls back to the
|
||||
// default icon.
|
||||
Icon string `json:"icon"`
|
||||
|
||||
|
|
@ -282,6 +283,10 @@ func (a createDocumentArgs) Validate() error {
|
|||
return errRequired("name")
|
||||
}
|
||||
|
||||
if a.Icon != "" {
|
||||
return icon.Validate(a.Icon)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +298,7 @@ func (createDocument) Info() Info {
|
|||
Description: "Create a document and return {document_id, branch_id}, the branch_id being its default branch. It starts with one empty paragraph; fill it with insert_blocks at position end on that branch.",
|
||||
Properties: map[string]any{
|
||||
"name": map[string]any{"type": "string", "description": "The display name."},
|
||||
document.AttrIcon: map[string]any{"type": "string", "description": "Optional. An Iconify id; the sidebar uses MingCute fills, such as mingcute:file-code-fill. Defaults to mingcute:document-2-fill."},
|
||||
document.AttrIcon: map[string]any{"type": "string", "description": "Optional. An Iconify id from the icon picker, mostly MingCute fills such as mingcute:file-code-fill; another id is refused, naming the closest ones. Defaults to mingcute:document-2-fill."},
|
||||
"parent_id": map[string]any{"type": "string", "description": "Optional. The parent document id; omit it for the organisation root."},
|
||||
},
|
||||
Required: []string{"name"},
|
||||
|
|
@ -324,14 +329,14 @@ func (createDocument) Execute(inp *input) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
|
||||
icon := in.Icon
|
||||
if icon == "" {
|
||||
icon = "mingcute:document-2-fill"
|
||||
iconID := in.Icon
|
||||
if iconID == "" {
|
||||
iconID = "mingcute:document-2-fill"
|
||||
}
|
||||
|
||||
doc := document.NewDocument(document.CreateInput{
|
||||
Name: in.Name,
|
||||
Icon: icon,
|
||||
Icon: iconID,
|
||||
ParentID: in.ParentID,
|
||||
}, inp.orgID, inp.userID)
|
||||
|
||||
|
|
@ -448,6 +453,12 @@ func (a updateDocumentArgs) Validate() error {
|
|||
return errors.New("give at least one of name, icon or parent_id")
|
||||
}
|
||||
|
||||
if a.Icon != "" {
|
||||
if err := icon.Validate(a.Icon); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if a.ParentID != nil && *a.ParentID != "" {
|
||||
if _, err := xid.FromString(*a.ParentID); err != nil {
|
||||
return fmt.Errorf("parent_id: %w", err)
|
||||
|
|
@ -511,7 +522,7 @@ func (updateDocument) Info() Info {
|
|||
Properties: map[string]any{
|
||||
"document_id": map[string]any{"type": "string", "description": _documentIDDescription},
|
||||
"name": map[string]any{"type": "string", "description": "Optional. The new display name."},
|
||||
document.AttrIcon: map[string]any{"type": "string", "description": "Optional. An Iconify id; the sidebar uses MingCute fills, such as mingcute:rocket-fill."},
|
||||
document.AttrIcon: map[string]any{"type": "string", "description": "Optional. An Iconify id from the icon picker, mostly MingCute fills such as mingcute:rocket-fill; another id is refused, naming the closest ones."},
|
||||
"parent_id": map[string]any{"type": "string", "description": "Optional. The new parent document id, or an empty string for the organisation root."},
|
||||
},
|
||||
Required: []string{"document_id"},
|
||||
|
|
|
|||
|
|
@ -323,6 +323,13 @@ func Test_createDocumentArgs_Validate(t *testing.T) {
|
|||
assertValidate(t, createDocumentArgs{Name: "n"}, map[string]Args{
|
||||
"name": createDocumentArgs{},
|
||||
})
|
||||
|
||||
require.NoError(t, createDocumentArgs{Name: "n", Icon: "mingcute:rocket-fill"}.Validate())
|
||||
|
||||
// an icon the picker does not offer is refused, naming close ones.
|
||||
err := createDocumentArgs{Name: "n", Icon: "lucide:rocket"}.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "mingcute:rocket-fill")
|
||||
}
|
||||
|
||||
func Test_createDocument_Info(t *testing.T) {
|
||||
|
|
@ -395,7 +402,7 @@ func Test_createDocument_Execute(t *testing.T) {
|
|||
},
|
||||
"Created under a parent": {
|
||||
DB: stubCreateDB(nil),
|
||||
Args: `{"name":"Runbook","icon":"lucide:rocket","parent_id":"` + parentID.String() + `"}`,
|
||||
Args: `{"name":"Runbook","icon":"mingcute:rocket-fill","parent_id":"` + parentID.String() + `"}`,
|
||||
Notify: 1,
|
||||
},
|
||||
}
|
||||
|
|
@ -616,7 +623,7 @@ func Test_updateDocumentArgs_Validate(t *testing.T) {
|
|||
})
|
||||
|
||||
// any one change is enough, and the root is a change.
|
||||
require.NoError(t, updateDocumentArgs{DocumentID: _testDocID, Icon: "i"}.Validate())
|
||||
require.NoError(t, updateDocumentArgs{DocumentID: _testDocID, Icon: "mingcute:rocket-fill"}.Validate())
|
||||
require.NoError(t, updateDocumentArgs{DocumentID: _testDocID, ParentID: &root}.Validate())
|
||||
|
||||
// a call that changes nothing is refused, named by what it takes.
|
||||
|
|
@ -628,6 +635,11 @@ func Test_updateDocumentArgs_Validate(t *testing.T) {
|
|||
err = updateDocumentArgs{DocumentID: _testDocID, ParentID: &bad}.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "parent_id")
|
||||
|
||||
// an icon the picker does not offer is refused, naming close ones.
|
||||
err = updateDocumentArgs{DocumentID: _testDocID, Icon: "lucide:rocket"}.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "mingcute:rocket-fill")
|
||||
}
|
||||
|
||||
func Test_updateDocumentArgs_parent(t *testing.T) {
|
||||
|
|
@ -709,8 +721,8 @@ func Test_updateDocument_Summary(t *testing.T) {
|
|||
"Icon": {Args: doc + `,"icon":"mingcute:rocket-fill"}`, Result: "Set the icon to mingcute:rocket-fill"},
|
||||
"Root": {Args: doc + `,"parent_id":""}`, Result: "Move it to the org root"},
|
||||
"Parent": {Args: doc + `,"parent_id":"` + xid.New().String() + `"}`, Result: "Move it under another document"},
|
||||
"Two": {Args: doc + `,"name":"Playbook","icon":"i"}`, Result: `Rename Runbook to "Playbook" and set the icon to i`},
|
||||
"All three": {Args: doc + `,"name":"Playbook","icon":"i","parent_id":""}`, Result: `Rename Runbook to "Playbook", set the icon to i and move it to the org root`},
|
||||
"Two": {Args: doc + `,"name":"Playbook","icon":"mingcute:rocket-fill"}`, Result: `Rename Runbook to "Playbook" and set the icon to mingcute:rocket-fill`},
|
||||
"All three": {Args: doc + `,"name":"Playbook","icon":"mingcute:rocket-fill","parent_id":""}`, Result: `Rename Runbook to "Playbook", set the icon to mingcute:rocket-fill and move it to the org root`},
|
||||
}
|
||||
|
||||
for cn, c := range cc {
|
||||
|
|
@ -799,20 +811,20 @@ func Test_updateDocument_Execute(t *testing.T) {
|
|||
"Icon set": {
|
||||
DB: stubDocumentDB(),
|
||||
Applier: stubApplier(),
|
||||
Args: doc + `,"icon":"i"}`,
|
||||
Args: doc + `,"icon":"mingcute:rocket-fill"}`,
|
||||
Ops: 1,
|
||||
Notify: 1,
|
||||
Result: `{"document_id":"` + _testDocID.String() + `","icon":"i"}`,
|
||||
Result: `{"document_id":"` + _testDocID.String() + `","icon":"mingcute:rocket-fill"}`,
|
||||
},
|
||||
// name and icon travel in one batch, so the live document
|
||||
// changes once.
|
||||
"Renamed and icon set": {
|
||||
DB: stubDocumentDB(),
|
||||
Applier: stubApplier(),
|
||||
Args: doc + `,"name":"Playbook","icon":"i"}`,
|
||||
Args: doc + `,"name":"Playbook","icon":"mingcute:rocket-fill"}`,
|
||||
Ops: 2,
|
||||
Notify: 1,
|
||||
Result: `{"document_id":"` + _testDocID.String() + `","name":"Playbook","icon":"i"}`,
|
||||
Result: `{"document_id":"` + _testDocID.String() + `","name":"Playbook","icon":"mingcute:rocket-fill"}`,
|
||||
},
|
||||
"Error returned by db.CheckDocumentExists": {
|
||||
DB: moveDB(assert.AnError, false, nil, nil),
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -9,6 +9,7 @@ import { icons as ph } from "@iconify-json/ph"
|
|||
import { icons as ri } from "@iconify-json/ri"
|
||||
import { icons as simpleIcons } from "@iconify-json/simple-icons"
|
||||
import { describe, it } from "vitest"
|
||||
import coreIcons from "~/../../server/core/internal/assistant/icon/icons.json"
|
||||
import { selectableIconList, selectableIcons } from "./icon"
|
||||
|
||||
const collections: Record<string, IconifyJSON | undefined> = {
|
||||
|
|
@ -23,6 +24,15 @@ const collections: Record<string, IconifyJSON | undefined> = {
|
|||
"simple-icons": simpleIcons,
|
||||
}
|
||||
|
||||
describe("selectableIcons", () => {
|
||||
// the assistant refuses icons outside its own copy of this list.
|
||||
it("matches the list core validates assistant icons against", ({
|
||||
expect,
|
||||
}) => {
|
||||
expect(selectableIcons).toEqual(coreIcons)
|
||||
})
|
||||
})
|
||||
|
||||
describe("selectableIconList", () => {
|
||||
it("returns the id of every selectable icon in order", ({ expect }) => {
|
||||
const list = selectableIconList()
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user