diff --git a/docs/userGuide/syntax/cardstacks.md b/docs/userGuide/syntax/cardstacks.md index c672f1c6eb..a04122e701 100644 --- a/docs/userGuide/syntax/cardstacks.md +++ b/docs/userGuide/syntax/cardstacks.md @@ -1,7 +1,8 @@ {% from "userGuide/components/advanced.md" import slot_info_trigger %} ## Card Stack -The Card Stack component allows you to display a collection of information in the form of a cards layout. The `cardstack` component acts as the container for `card` components each containing the content you want to show. + +The Card Stack component allows you to display a collection of information in the form of a cards layout. The `cardstack` component acts as the container for `card` components each containing the content you want to show. A `cardstack` component is used in conjunction with one or more `card` components. - `cardstack`: Wrapper used to hold cards and their content. @@ -9,18 +10,18 @@ A `cardstack` component is used in conjunction with one or more `card` component Each `card` contains `tag` and `keyword` field: - `Keywords`: Adds to the search space but does not allow users to filter them manually. Add keywords when the content have different known aliases. -- `Tags`: Adds to the search space and also provides readers a way to filter the cards according to the selected tags. Add tags to categorise the content. +- `Tags`: Adds to the search space and also provides readers a way to filter the cards according to the selected tags. Add tags to categorise the content. -The search feature searches the `card` components of `cardstack` by header, tags and keywords specified within each card component. +The search feature searches the `card` components of `cardstack` by header, tags and keywords specified within each card component. Specifying them can help improve searchability of the `cardstack` component! For example, if a card is about "Machine Learning," you might tag it as `AI` and `Data Science` and add keywords like `ML` and `Artificial Intelligence` to improve searchability. - + html @@ -135,24 +136,98 @@ In the example given below, a Card Stack is used to show a list of questions and The example above also illustrates how to use the `keywords` attribute to specify additional search terms for a card. -****Options**** +### Custom Tag Order and Colors + +You can customize the order and colors of tags by using a `` element inside the `cardstack`: + + +html + + + + + + + + + + + + Success is not final, failure is not fatal: it is the courage to continue that counts + + + In the middle of every difficulty lies opportunity + + + Do what you can, with what you have, where you are + + + Your time is limited, so don't waste it living someone else's life + + + + + +You can also use Bootstrap color names instead of hex colors: + + +html + + + + + + + + + + Success is not final, failure is not fatal: it is the courage to continue that counts + + + In the middle of every difficulty lies opportunity + + + Do what you can, with what you have, where you are + + + + + +The `` element allows you to: +- Specify the order in which tags appear in the filter badges +- Assign custom colors to each tag using either: + - Hex format (e.g., `#28a745`) + - Bootstrap color names (e.g., `success`, `danger`, `primary`, `warning`, `info`, `secondary`, `light`, `dark`) +- Any tags used in cards but not defined in `` will appear after the defined tags with default colors + +**Options** `cardstack`: -Name | Type | Default | Description ---- | --- | --- | --- -blocks | `String` | `2` | Number of `card` columns per row.
Supports: `1`, `2`, `3`, `4`, `6` -searchable | `Boolean` | `false` | Whether the card stack is searchable. -show-select-all | `Boolean` | `true` | Whether the select all tag button appears. (`false` by default if total tags ≤ 3) -`card`: -Name | Type | Default | Description ---- | --- | --- | --- -tag | `String` | `null` | Tags of each card component.
Each unique tag should be seperated by a `,`.
Tags are added to the search field. -header | `String` | `null` | Header of each card component.
Supports the use of inline markdown elements. -keywords | `String` | `null` | Keywords of each card component.
Each unique keyword should be seperated by a `,`.
Keywords are added to the search field. -disabled | `Boolean` | `false` | Disable card.
This removes visibility of the card and makes it unsearchable. +| Name | Type | Default | Description | +| --------------- | --------- | ------- | --------------------------------------------------------------------------------- | +| blocks | `String` | `2` | Number of `card` columns per row.
Supports: `1`, `2`, `3`, `4`, `6` | +| searchable | `Boolean` | `false` | Whether the card stack is searchable. | +| show-select-all | `Boolean` | `true` | Whether the select all tag button appears. (`false` by default if total tags ≤ 3) | + +`tags` (optional): +A container element inside `cardstack` to define tag ordering and colors. + +`tag` (inside `tags` element): +| Name | Type | Default | Description | +| ----- | -------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | `String` | (required) | The name of the tag (must match tags used in cards). | +| color | `String` | (auto) | Custom color for the tag.
Supports hex format (e.g., `#28a745`) or Bootstrap color names (e.g., `success`, `danger`, `primary`).
If not specified, uses default Bootstrap color scheme. | + +`card`: +| Name | Type | Default | Description | +| -------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- | +| tag | `String` | `null` | Tags of each card component.
Each unique tag should be separated by a `,`.
Tags are added to the search field. | +| header | `String` | `null` | Header of each card component.
Supports the use of inline markdown elements. | +| keywords | `String` | `null` | Keywords of each card component.
Each unique keyword should be separated by a `,`.
Keywords are added to the search field. | +| disabled | `Boolean` | `false` | Disable card.
This removes visibility of the card and makes it unsearchable. |
diff --git a/package-lock.json b/package-lock.json index 041d714f71..6c6ab288f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -1991,6 +1992,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": "^14 || ^16 || >=18" }, @@ -2013,6 +2015,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": "^14 || ^16 || >=18" } @@ -4569,6 +4572,7 @@ "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -5321,6 +5325,7 @@ "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -5356,6 +5361,7 @@ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -6267,6 +6273,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6320,6 +6327,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6725,6 +6733,7 @@ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -6781,6 +6790,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7314,6 +7324,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -9477,6 +9488,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -10424,17 +10436,6 @@ "node": ">= 0.8" } }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -10774,6 +10775,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", @@ -10921,6 +10923,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -11825,6 +11828,7 @@ "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tabbable": "^6.4.0" } @@ -12791,10 +12795,20 @@ "license": "ISC" }, "node_modules/html-entities": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", - "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", - "dev": true + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" }, "node_modules/html-escaper": { "version": "2.0.2", @@ -16069,6 +16083,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -17058,6 +17073,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@napi-rs/wasm-runtime": "0.2.4", "@yarnpkg/lockfile": "^1.1.0", @@ -18098,6 +18114,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -18207,7 +18224,6 @@ "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.6.0.tgz", "integrity": "sha512-OWgQ9/Pe23MnNJC0PL4uZp8k0EDaUvqpJFSiwFxOLClAhmD7UEisyhO3x5hVsD4xFrjReVTXydlrMes45dJ71w==", "dev": true, - "peer": true, "dependencies": { "htmlparser2": "^8.0.0", "js-tokens": "^8.0.0", @@ -18223,7 +18239,6 @@ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, - "peer": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -18243,15 +18258,13 @@ "type": "github", "url": "https://github.com/sponsors/fb55" } - ], - "peer": true + ] }, "node_modules/postcss-html/node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, - "peer": true, "dependencies": { "domelementtype": "^2.3.0" }, @@ -18267,7 +18280,6 @@ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dev": true, - "peer": true, "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -18282,7 +18294,6 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, - "peer": true, "engines": { "node": ">=0.12" }, @@ -18302,7 +18313,6 @@ "url": "https://github.com/sponsors/fb55" } ], - "peer": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", @@ -18314,15 +18324,13 @@ "version": "8.0.3", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/postcss-html/node_modules/postcss-safe-parser": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", "dev": true, - "peer": true, "engines": { "node": ">=12.0" }, @@ -18766,6 +18774,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", "dev": true, + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -20886,6 +20895,7 @@ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.2.1.tgz", "integrity": "sha512-SfIMGFK+4n7XVAyv50CpVfcGYWG4v41y6xG7PqOgQSY8M/PgdK0SQbjWFblxjJZlN9jNq879mB4BCZHJRIJ1hA==", "dev": true, + "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3", @@ -21883,6 +21893,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -22195,7 +22206,8 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/tsutils": { "version": "3.21.0", @@ -22293,6 +22305,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -22626,6 +22639,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.11.tgz", "integrity": "sha512-d4oBctG92CRO1cQfVBZp6WJAs0n8AK4Xf5fNjQCBeKCvMI1efGQ5E3Alt1slFJS9fZuPcFoiAiqFvQlv1X7t/w==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.3.11", "@vue/compiler-sfc": "3.3.11", @@ -22852,6 +22866,7 @@ "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -22899,6 +22914,7 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", @@ -22980,6 +22996,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -23083,6 +23100,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -23309,6 +23327,7 @@ "version": "2.4.7", "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.7.tgz", "integrity": "sha512-vLB4BqzCKDnnZH9PHGoS2ycawueX4HLqENXQitvFHczhgW2vFpSOn31LZtVr1KU8YTw7DS4tM+cqyovxo8taVg==", + "peer": true, "dependencies": { "async": "^2.6.4", "colors": "1.0.x", @@ -25208,6 +25227,7 @@ "fs-extra": "^9.0.1", "gh-pages": "^6.3.0", "highlight.js": "^10.4.1", + "html-entities": "^2.6.0", "htmlparser2": "^3.10.1", "ignore": "^5.1.4", "js-beautify": "1.14.3", @@ -25841,6 +25861,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -26574,6 +26595,9 @@ "name": "@markbind/vue-components", "version": "6.1.0", "license": "MIT", + "dependencies": { + "html-entities": "^2.6.0" + }, "devDependencies": { "@babel/core": "^7.26.9", "@babel/plugin-transform-runtime": "^7.26.9", @@ -27076,6 +27100,7 @@ "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", @@ -27379,6 +27404,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -28307,6 +28333,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", "dev": true, + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -29576,13 +29603,15 @@ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.5.0.tgz", "integrity": "sha512-abypo6m9re3clXA00eu5syw+oaPHbJTPapu9C4pzNsJ4hdZDzushT50Zhu+iIYXgEe1CxnRMn7ngsbV+MLrlpQ==", "dev": true, + "peer": true, "requires": {} }, "@csstools/css-tokenizer": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.3.tgz", "integrity": "sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg==", - "dev": true + "dev": true, + "peer": true }, "@csstools/media-query-list-parser": { "version": "2.1.7", @@ -30739,6 +30768,7 @@ "fs-extra": "^9.0.1", "gh-pages": "^6.3.0", "highlight.js": "^10.4.1", + "html-entities": "^2.6.0", "htmlparser2": "^3.10.1", "ignore": "^5.1.4", "jest": "^29.7.0", @@ -31152,6 +31182,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "requires": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -31726,6 +31757,7 @@ "css-loader": "^3.6.0", "eslint-plugin-vue": "^9.33.0", "floating-vue": "^5.2.2", + "html-entities": "^2.6.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "portal-vue": "^3.0.0", @@ -32087,6 +32119,7 @@ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, + "peer": true, "requires": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", @@ -32288,6 +32321,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "requires": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -33485,6 +33519,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "dev": true, + "peer": true, "requires": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -34113,6 +34148,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "dev": true, + "peer": true, "requires": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -34131,6 +34167,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, + "peer": true, "requires": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -34719,7 +34756,8 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -34758,6 +34796,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -35035,6 +35074,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "dev": true, + "peer": true, "requires": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -35078,6 +35118,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "requires": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -35437,6 +35478,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -37001,6 +37043,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -37702,16 +37745,6 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "optional": true, - "requires": { - "iconv-lite": "^0.6.2" - } - }, "end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -37969,6 +38002,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dev": true, + "peer": true, "requires": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", @@ -38102,6 +38136,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", "dev": true, + "peer": true, "requires": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -38779,6 +38814,7 @@ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", "dev": true, + "peer": true, "requires": { "tabbable": "^6.4.0" } @@ -39461,10 +39497,9 @@ } }, "html-entities": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", - "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", - "dev": true + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==" }, "html-escaper": { "version": "2.0.2", @@ -43053,6 +43088,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -43789,6 +43825,7 @@ "resolved": "https://registry.npmjs.org/nx/-/nx-20.8.3.tgz", "integrity": "sha512-8w815WSMWar3A/LFzwtmEY+E8cVW62lMiFuPDXje+C8O8hFndfvscP56QHNMn2Zdhz3q0+BZUe+se4Em1BKYdA==", "dev": true, + "peer": true, "requires": { "@napi-rs/wasm-runtime": "0.2.4", "@nx/nx-darwin-arm64": "20.8.3", @@ -44506,6 +44543,7 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "peer": true, "requires": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -44577,7 +44615,6 @@ "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.6.0.tgz", "integrity": "sha512-OWgQ9/Pe23MnNJC0PL4uZp8k0EDaUvqpJFSiwFxOLClAhmD7UEisyhO3x5hVsD4xFrjReVTXydlrMes45dJ71w==", "dev": true, - "peer": true, "requires": { "htmlparser2": "^8.0.0", "js-tokens": "^8.0.0", @@ -44590,7 +44627,6 @@ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, - "peer": true, "requires": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -44601,15 +44637,13 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "peer": true + "dev": true }, "domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, - "peer": true, "requires": { "domelementtype": "^2.3.0" } @@ -44619,7 +44653,6 @@ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dev": true, - "peer": true, "requires": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -44630,15 +44663,13 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "peer": true + "dev": true }, "htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", "dev": true, - "peer": true, "requires": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", @@ -44650,15 +44681,13 @@ "version": "8.0.3", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", - "dev": true, - "peer": true + "dev": true }, "postcss-safe-parser": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", "dev": true, - "peer": true, "requires": {} } } @@ -44959,6 +44988,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz", "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==", "dev": true, + "peer": true, "requires": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -46508,6 +46538,7 @@ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.2.1.tgz", "integrity": "sha512-SfIMGFK+4n7XVAyv50CpVfcGYWG4v41y6xG7PqOgQSY8M/PgdK0SQbjWFblxjJZlN9jNq879mB4BCZHJRIJ1hA==", "dev": true, + "peer": true, "requires": { "@csstools/css-parser-algorithms": "^2.5.0", "@csstools/css-tokenizer": "^2.2.3", @@ -47218,7 +47249,8 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true + "dev": true, + "peer": true } } }, @@ -47455,7 +47487,8 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true + "dev": true, + "peer": true }, "tsutils": { "version": "3.21.0", @@ -47527,7 +47560,8 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true + "devOptional": true, + "peer": true }, "uc.micro": { "version": "1.0.6", @@ -47757,6 +47791,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.11.tgz", "integrity": "sha512-d4oBctG92CRO1cQfVBZp6WJAs0n8AK4Xf5fNjQCBeKCvMI1efGQ5E3Alt1slFJS9fZuPcFoiAiqFvQlv1X7t/w==", + "peer": true, "requires": { "@vue/compiler-dom": "3.3.11", "@vue/compiler-sfc": "3.3.11", @@ -47922,6 +47957,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", "dev": true, + "peer": true, "requires": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -47959,6 +47995,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "peer": true, "requires": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -48049,6 +48086,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, + "peer": true, "requires": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", @@ -48091,6 +48129,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -48229,6 +48268,7 @@ "version": "2.4.7", "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.7.tgz", "integrity": "sha512-vLB4BqzCKDnnZH9PHGoS2ycawueX4HLqENXQitvFHczhgW2vFpSOn31LZtVr1KU8YTw7DS4tM+cqyovxo8taVg==", + "peer": true, "requires": { "async": "^2.6.4", "colors": "1.0.x", diff --git a/packages/core/package.json b/packages/core/package.json index 2ba7e9ef4d..bf00251f1e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -42,6 +42,7 @@ "fs-extra": "^9.0.1", "gh-pages": "^6.3.0", "highlight.js": "^10.4.1", + "html-entities": "^2.6.0", "htmlparser2": "^3.10.1", "ignore": "^5.1.4", "js-beautify": "1.14.3", diff --git a/packages/core/src/html/MdAttributeRenderer.ts b/packages/core/src/html/MdAttributeRenderer.ts index 442689d883..f454763ff3 100644 --- a/packages/core/src/html/MdAttributeRenderer.ts +++ b/packages/core/src/html/MdAttributeRenderer.ts @@ -9,6 +9,8 @@ const _ = { has, }; +export type CardStackTagConfig = { name: string; color?: string }; + /** * Class that is responsible for rendering markdown-in-attributes */ @@ -30,7 +32,7 @@ export class MdAttributeRenderer { processAttributeWithoutOverride(node: MbNode, attribute: string, isInline: boolean, slotName = attribute): void { const hasAttributeSlot = node.children - && node.children.some(child => getVslotShorthandName(child) === slotName); + && node.children.some(child => getVslotShorthandName(child) === slotName); if (!hasAttributeSlot && _.has(node.attribs, attribute)) { let rendered; @@ -167,6 +169,8 @@ export class MdAttributeRenderer { this.processSlotAttribute(node, 'header', true); } + // eslint-disable-next-line class-methods-use-this + /* * Dropdowns */ diff --git a/packages/core/src/html/NodeProcessor.ts b/packages/core/src/html/NodeProcessor.ts index ee8e8ec8d4..3bb857a6d7 100644 --- a/packages/core/src/html/NodeProcessor.ts +++ b/packages/core/src/html/NodeProcessor.ts @@ -27,6 +27,7 @@ import { setHeadingId, assignPanelId } from './headerProcessor'; import { FootnoteProcessor } from './FootnoteProcessor'; import { MbNode, NodeOrText, TextElement } from '../utils/node'; import { processUlNode } from './CustomListIconProcessor'; +import { processCardStackAttributes } from './cardStackProcessor'; const fm = require('fastmatter'); @@ -214,6 +215,9 @@ export class NodeProcessor { case 'card': this.mdAttributeRenderer.processCardAttributes(node); break; + case 'cardstack': + processCardStackAttributes(node); + break; case 'modal': this.processModal(node); break; diff --git a/packages/core/src/html/cardStackProcessor.ts b/packages/core/src/html/cardStackProcessor.ts new file mode 100644 index 0000000000..7670cd951c --- /dev/null +++ b/packages/core/src/html/cardStackProcessor.ts @@ -0,0 +1,55 @@ +import { encode } from 'html-entities'; +import { MbNode, NodeOrText } from '../utils/node'; +import { CardStackTagConfig } from './MdAttributeRenderer'; + +function isTag(child: NodeOrText): child is MbNode { + return child.type === 'tag' && (child as MbNode).name === 'tag'; +} + +function isTags(child: NodeOrText): child is MbNode { + return child.type === 'tag' && (child as MbNode).name === 'tags'; +} + +export function processCardStackAttributes(node: MbNode) { + // Look for a child element + if (!node.children) { + return; + } + + const tagsNodeIndex = node.children.findIndex( + child => isTags(child), + ); + + if (tagsNodeIndex === -1) { + return; + } + + const tagsNode = node.children[tagsNodeIndex] as MbNode; + const tagConfigs: Array = []; + + // Parse each element + if (tagsNode.children) { + tagsNode.children.forEach((child) => { + if (isTag(child)) { + if (child.attribs?.name) { + const config: CardStackTagConfig = { + name: child.attribs.name, + ...(child.attribs.color && { color: child.attribs.color }), + }; + tagConfigs.push(config); + } + } + }); + } + + // Add tag-configs as a prop if we found any tags + if (tagConfigs.length > 0) { + const jsonString = JSON.stringify(tagConfigs); + // Replace double quotes with HTML entities to avoid SSR warnings + const escapedJson = encode(jsonString); + node.attribs['data-tag-configs'] = escapedJson; + } + + // Remove the node from the DOM tree + node.children.splice(tagsNodeIndex, 1); +} diff --git a/packages/core/src/html/codeblockProcessor.ts b/packages/core/src/html/codeblockProcessor.ts index 85689be6da..55e04f375c 100644 --- a/packages/core/src/html/codeblockProcessor.ts +++ b/packages/core/src/html/codeblockProcessor.ts @@ -1,9 +1,9 @@ +import { decode } from 'html-entities'; import cheerio from 'cheerio'; import has from 'lodash/has'; import { NodeOrText, MbNode } from '../utils/node'; import md from '../lib/markdown-it'; -import * as util from '../lib/markdown-it/utils'; const _ = { has, @@ -46,7 +46,7 @@ function traverseLinePart( * so to actually highlight this text, we have to ask to apply at its parent. */ - const cleanedText = util.unescapeHtml(node.data); + const cleanedText = decode(node.data); const textLength = cleanedText.length; resData.numCharsTraversed = textLength; @@ -129,7 +129,7 @@ function traverseLinePart( } } else { const [start, end] = data.highlightRange; - const cleaned = util.unescapeHtml(child.data); + const cleaned = decode(child.data); const split = [cleaned.substring(0, start), cleaned.substring(start, end), cleaned.substring(end)]; const [pre, highlighted, post] = split.map(md.utils.escapeHtml); diff --git a/packages/core/src/lib/markdown-it/utils/index.ts b/packages/core/src/lib/markdown-it/utils/index.ts deleted file mode 100644 index 589456ce47..0000000000 --- a/packages/core/src/lib/markdown-it/utils/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - Extra utility functions related to markdown-it. - markdown-it library exposes a utility module in markdown-it/utils, - below are additional functions that can be used as helpers alongside markdown-it/utils - */ - -// This mapping is taken from markdown-it/utils, just flipped. -// Refer to the original file at markdown-it/lib/common/utils.js -const htmlUnescapedMapping = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - ''': '\'', -}; - -// markdown-it/utils have an escapeHtml function, but not the -// complementary un-escaping function - -// Used in highlighting calculations since -// markdown-it stores text as escaped HTML entities (e.g. <, >). -// To correctly measure character positions and split text for partial -// highlights, we must first decode entities back into their real characters. - -/** - * Replaces HTML escape sequences in the input string with their corresponding unescaped characters. - */ -export function unescapeHtml(str: string) { - let unescaped = str; - Object.entries(htmlUnescapedMapping).forEach(([key, value]) => { - unescaped = unescaped.split(key).join(value); - }); - return unescaped; -} diff --git a/packages/core/test/unit/html/MdAttributeRenderer.test.ts b/packages/core/test/unit/html/MdAttributeRenderer.test.ts new file mode 100644 index 0000000000..08a7bc102c --- /dev/null +++ b/packages/core/test/unit/html/MdAttributeRenderer.test.ts @@ -0,0 +1,218 @@ +import { processCardStackAttributes } from '../../../src/html/cardStackProcessor'; +import { MbNode, parseHTML } from '../../../src/utils/node'; + +describe('processCardStackAttributes', () => { + it('should do nothing when node has no children', () => { + const node: MbNode = { + type: 'tag', + name: 'cardstack', + attribs: {}, + children: undefined, + } as MbNode; + + processCardStackAttributes(node); + + expect(node.attribs['data-tag-configs']).toBeUndefined(); + }); + + it('should do nothing when there is no child element', () => { + const html = 'Content'; + const nodes = parseHTML(html); + const cardstackNode = nodes[0] as MbNode; + + processCardStackAttributes(cardstackNode); + + expect(cardstackNode.attribs['data-tag-configs']).toBeUndefined(); + }); + + it('should parse element with single tag config', () => { + const html = ` + + + + Content + `; + const nodes = parseHTML(html); + const cardstackNode = nodes[0] as MbNode; + + processCardStackAttributes(cardstackNode); + + expect(cardstackNode.attribs['data-tag-configs']).toBeDefined(); + const decodedConfig = cardstackNode.attribs['data-tag-configs'] + .replace(/"/g, '"'); + const parsed = JSON.parse(decodedConfig); + expect(parsed).toEqual([{ name: 'Success', color: '#28a745' }]); + }); + + it('should parse element with multiple tag configs', () => { + const html = ` + + + + + + Content + `; + const nodes = parseHTML(html); + const cardstackNode = nodes[0] as MbNode; + + processCardStackAttributes(cardstackNode); + + expect(cardstackNode.attribs['data-tag-configs']).toBeDefined(); + const decodedConfig = cardstackNode.attribs['data-tag-configs'] + .replace(/"/g, '"'); + const parsed = JSON.parse(decodedConfig); + expect(parsed).toEqual([ + { name: 'Success', color: '#28a745' }, + { name: 'Failure', color: '#dc3545' }, + { name: 'Neutral', color: '#6c757d' }, + ]); + }); + + it('should parse tags with Bootstrap color names', () => { + const html = ` + + + + + Content + `; + const nodes = parseHTML(html); + const cardstackNode = nodes[0] as MbNode; + + processCardStackAttributes(cardstackNode); + + const decodedConfig = cardstackNode.attribs['data-tag-configs'] + .replace(/"/g, '"'); + const parsed = JSON.parse(decodedConfig); + expect(parsed).toEqual([ + { name: 'Success', color: 'success' }, + { name: 'Danger', color: 'danger' }, + ]); + }); + + it('should parse tags without color attribute', () => { + const html = ` + + + + + Content + `; + const nodes = parseHTML(html); + const cardstackNode = nodes[0] as MbNode; + + processCardStackAttributes(cardstackNode); + + const decodedConfig = cardstackNode.attribs['data-tag-configs'] + .replace(/"/g, '"'); + const parsed = JSON.parse(decodedConfig); + expect(parsed).toEqual([ + { name: 'Success' }, + { name: 'Failure', color: '#dc3545' }, + ]); + }); + + it('should ignore elements without name attribute', () => { + const html = ` + + + + + Content + `; + const nodes = parseHTML(html); + const cardstackNode = nodes[0] as MbNode; + + processCardStackAttributes(cardstackNode); + + expect(cardstackNode.attribs['data-tag-configs']).toBeDefined(); + const decodedConfig = cardstackNode.attribs['data-tag-configs'] + .replace(/"/g, '"'); + const parsed = JSON.parse(decodedConfig); + expect(parsed).toEqual([{ name: 'Valid', color: '#dc3545' }]); + }); + + it('should remove the node from the DOM tree', () => { + const html = ` + + + + Content + `; + const nodes = parseHTML(html); + const cardstackNode = nodes[0] as MbNode; + const initialChildCount = cardstackNode.children?.length || 0; + + processCardStackAttributes(cardstackNode); + + // Should have removed the element + const hasTagsNode = cardstackNode.children?.some( + child => child.type === 'tag' && (child as MbNode).name === 'tags', + ); + expect(hasTagsNode).toBe(false); + expect((cardstackNode.children?.length || 0)).toBeLessThan(initialChildCount); + }); + + it('should escape HTML entities in the data attribute', () => { + const html = ` + + + + Content + `; + const nodes = parseHTML(html); + const cardstackNode = nodes[0] as MbNode; + + processCardStackAttributes(cardstackNode); + + const dataAttr = cardstackNode.attribs['data-tag-configs']; + // Should contain escaped quotes + expect(dataAttr).toContain('"'); + // Should contain escaped HTML entities for the tag name + expect(dataAttr).toContain('<'); + expect(dataAttr).toContain('>'); + }); + + it('should handle empty element', () => { + const html = ` + + Content + `; + const nodes = parseHTML(html); + const cardstackNode = nodes[0] as MbNode; + + processCardStackAttributes(cardstackNode); + + // Should remove tags node but not add data-tag-configs since no tags were found + expect(cardstackNode.attribs['data-tag-configs']).toBeUndefined(); + const hasTagsNode = cardstackNode.children?.some( + child => child.type === 'tag' && (child as MbNode).name === 'tags', + ); + expect(hasTagsNode).toBe(false); + }); + + it('should handle with non-tag children', () => { + const html = ` + + +
Invalid
+ +
+ Content +
`; + const nodes = parseHTML(html); + const cardstackNode = nodes[0] as MbNode; + + processCardStackAttributes(cardstackNode); + + const decodedConfig = cardstackNode.attribs['data-tag-configs'] + .replace(/"/g, '"'); + const parsed = JSON.parse(decodedConfig); + // Should only include the elements, not
+ expect(parsed).toEqual([ + { name: 'Valid', color: '#28a745' }, + { name: 'AnotherValid', color: '#dc3545' }, + ]); + }); +}); diff --git a/packages/core/test/unit/lib/markdown-it/utils/index.test.ts b/packages/core/test/unit/lib/markdown-it/utils/index.test.ts deleted file mode 100644 index 3c48d01cc0..0000000000 --- a/packages/core/test/unit/lib/markdown-it/utils/index.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { unescapeHtml } from '../../../../../src/lib/markdown-it/utils'; - -describe('unescapeHtml', () => { - test('should unescape HTML entities and handle mixed content', () => { - const input = 'Hello & welcome to <MarkBind>! &<>"''; - const expected = 'Hello & welcome to ! &<>"\''; - const result = unescapeHtml(input); - expect(result).toBe(expected); - }); -}); diff --git a/packages/vue-components/package.json b/packages/vue-components/package.json index d2f667b5da..3cf6a2c805 100644 --- a/packages/vue-components/package.json +++ b/packages/vue-components/package.json @@ -44,5 +44,8 @@ "vue-final-modal": "^4.5.5", "vue-style-loader": "^4.1.3" }, - "private": true + "private": true, + "dependencies": { + "html-entities": "^2.6.0" + } } diff --git a/packages/vue-components/src/__tests__/CardStack.spec.js b/packages/vue-components/src/__tests__/CardStack.spec.js index 8589fe2a9f..dc66604179 100644 --- a/packages/vue-components/src/__tests__/CardStack.spec.js +++ b/packages/vue-components/src/__tests__/CardStack.spec.js @@ -42,6 +42,12 @@ const MARKDOWN_CARDS = ` `; +const CARDS_WITH_CUSTOM_TAGS = ` + + + +`; + describe('CardStack', () => { test('should not hide cards when no filter is provided', async () => { const wrapper = mount(CardStack, { @@ -228,4 +234,130 @@ describe('CardStack', () => { const selectAllBadge = wrapper.find('.select-all-toggle'); expect(selectAllBadge.exists()).toBe(false); }); + + test('should respect custom tag order from tag-configs', async () => { + const tagConfigs = JSON.stringify([ + { name: 'Neutral', color: '#6c757d' }, + { name: 'Success', color: '#28a745' }, + { name: 'Failure', color: '#dc3545' }, + ]); + const wrapper = mount(CardStack, { + propsData: { + dataTagConfigs: tagConfigs.replace(/"/g, '"'), + }, + slots: { default: CARDS_WITH_CUSTOM_TAGS }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + const { tagMapping } = wrapper.vm.cardStackRef; + expect(tagMapping.length).toBe(3); + expect(tagMapping[0][0]).toBe('Neutral'); + expect(tagMapping[1][0]).toBe('Success'); + expect(tagMapping[2][0]).toBe('Failure'); + }); + + test('should apply custom hex colors from tag-configs', async () => { + const tagConfigs = JSON.stringify([ + { name: 'Success', color: '#28a745' }, + { name: 'Failure', color: '#dc3545' }, + ]); + const wrapper = mount(CardStack, { + propsData: { + dataTagConfigs: tagConfigs.replace(/"/g, '"'), + }, + slots: { default: CARDS_WITH_CUSTOM_TAGS }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + const { tagMapping } = wrapper.vm.cardStackRef; + expect(tagMapping[0][1].badgeColor).toBe('#28a745'); + expect(tagMapping[1][1].badgeColor).toBe('#dc3545'); + }); + + test('should convert Bootstrap color names to classes', async () => { + const tagConfigs = JSON.stringify([ + { name: 'Success', color: 'success' }, + { name: 'Failure', color: 'danger' }, + { name: 'Neutral', color: 'warning' }, + ]); + const wrapper = mount(CardStack, { + propsData: { + dataTagConfigs: tagConfigs.replace(/"/g, '"'), + }, + slots: { default: CARDS_WITH_CUSTOM_TAGS }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + const { tagMapping } = wrapper.vm.cardStackRef; + expect(tagMapping[0][1].badgeColor).toBe('bg-success'); + expect(tagMapping[1][1].badgeColor).toBe('bg-danger'); + expect(tagMapping[2][1].badgeColor).toBe('bg-warning text-dark'); + }); + + test('should use default colors for unconfigured tags', async () => { + const tagConfigs = JSON.stringify([{ name: 'Success', color: '#28a745' }]); + const wrapper = mount(CardStack, { + propsData: { + dataTagConfigs: tagConfigs.replace(/"/g, '"'), + }, + slots: { default: CARDS_WITH_CUSTOM_TAGS }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + const { tagMapping } = wrapper.vm.cardStackRef; + // Success should have custom color + expect(tagMapping[0][1].badgeColor).toBe('#28a745'); + // Other tags should have default Bootstrap colors + expect(tagMapping[1][1].badgeColor).toMatch(/^bg-/); + expect(tagMapping[2][1].badgeColor).toMatch(/^bg-/); + }); + + test('should handle invalid tag-configs gracefully', async () => { + const wrapper = mount(CardStack, { + propsData: { + dataTagConfigs: 'invalid-json', + }, + slots: { default: CARDS_WITH_CUSTOM_TAGS }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + // Should still render with default colors + const { tagMapping } = wrapper.vm.cardStackRef; + expect(tagMapping.length).toBe(3); + expect(tagMapping[0][1].badgeColor).toMatch(/^bg-/); + }); + + test('isBootstrapColor should correctly identify Bootstrap colors', async () => { + const wrapper = mount(CardStack, { + slots: { default: CARDS_WITH_CUSTOM_TAGS }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.isBootstrapColor('bg-primary')).toBe(true); + expect(wrapper.vm.isBootstrapColor('bg-warning text-dark')).toBe(true); + expect(wrapper.vm.isBootstrapColor('#28a745')).toBe(false); + expect(wrapper.vm.isBootstrapColor('custom-color')).toBe(false); + }); + + test('getTextColor should return correct contrast color', async () => { + const wrapper = mount(CardStack, { + slots: { default: CARDS_WITH_CUSTOM_TAGS }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + // Light background should have dark text + expect(wrapper.vm.getTextColor('#ffffff')).toBe('#000'); + expect(wrapper.vm.getTextColor('#f0f0f0')).toBe('#000'); + + // Dark background should have light text + expect(wrapper.vm.getTextColor('#000000')).toBe('#fff'); + expect(wrapper.vm.getTextColor('#333333')).toBe('#fff'); + }); }); diff --git a/packages/vue-components/src/__tests__/colors.spec.js b/packages/vue-components/src/__tests__/colors.spec.js new file mode 100644 index 0000000000..02d4755ffa --- /dev/null +++ b/packages/vue-components/src/__tests__/colors.spec.js @@ -0,0 +1,132 @@ +import { + BADGE_COLOURS, + MIN_TAGS_FOR_SELECT_ALL, + isBootstrapColor, + getTextColor, + normalizeColor, +} from '../utils/colors'; + +describe('colors.js', () => { + describe('isBootstrapColor', () => { + it('should return true for valid Bootstrap color classes', () => { + expect(isBootstrapColor('bg-primary')).toBe(true); + expect(isBootstrapColor('bg-secondary')).toBe(true); + expect(isBootstrapColor('bg-success')).toBe(true); + expect(isBootstrapColor('bg-danger')).toBe(true); + expect(isBootstrapColor('bg-warning text-dark')).toBe(true); + expect(isBootstrapColor('bg-info text-dark')).toBe(true); + expect(isBootstrapColor('bg-light text-dark')).toBe(true); + expect(isBootstrapColor('bg-dark')).toBe(true); + }); + + it('should return false for hex colors', () => { + expect(isBootstrapColor('#28a745')).toBe(false); + expect(isBootstrapColor('#dc3545')).toBe(false); + expect(isBootstrapColor('#ffffff')).toBe(false); + }); + + it('should return false for invalid color values', () => { + expect(isBootstrapColor('custom-color')).toBe(false); + expect(isBootstrapColor('bg-custom')).toBe(false); + expect(isBootstrapColor('')).toBe(false); + }); + }); + + describe('getTextColor', () => { + it('should return black for light backgrounds', () => { + expect(getTextColor('#ffffff')).toBe('#000'); + expect(getTextColor('#f0f0f0')).toBe('#000'); + expect(getTextColor('#ffc107')).toBe('#000'); // warning yellow + }); + + it('should return white for dark backgrounds', () => { + expect(getTextColor('#000000')).toBe('#fff'); + expect(getTextColor('#333333')).toBe('#fff'); + expect(getTextColor('#dc3545')).toBe('#fff'); // danger red + expect(getTextColor('#28a745')).toBe('#fff'); // success green + expect(getTextColor('#17a2b8')).toBe('#fff'); // info cyan + }); + + it('should return black for Bootstrap color classes', () => { + expect(getTextColor('bg-primary')).toBe('#000'); + expect(getTextColor('bg-warning text-dark')).toBe('#000'); + }); + + it('should handle edge cases', () => { + expect(getTextColor('')).toBe('#000'); + expect(getTextColor(null)).toBe('#000'); + expect(getTextColor(undefined)).toBe('#000'); + }); + + it('should handle colors without # prefix', () => { + expect(getTextColor('ffffff')).toBe('#000'); + expect(getTextColor('000000')).toBe('#fff'); + }); + }); + + describe('normalizeColor', () => { + it('should return null for empty or undefined values', () => { + expect(normalizeColor(null)).toBe(null); + expect(normalizeColor(undefined)).toBe(null); + expect(normalizeColor('')).toBe(null); + }); + + it('should return hex colors as-is', () => { + expect(normalizeColor('#28a745')).toBe('#28a745'); + expect(normalizeColor('#dc3545')).toBe('#dc3545'); + expect(normalizeColor('#ffffff')).toBe('#ffffff'); + }); + + it('should convert Bootstrap color names to classes', () => { + expect(normalizeColor('primary')).toBe('bg-primary'); + expect(normalizeColor('secondary')).toBe('bg-secondary'); + expect(normalizeColor('success')).toBe('bg-success'); + expect(normalizeColor('danger')).toBe('bg-danger'); + expect(normalizeColor('dark')).toBe('bg-dark'); + }); + + it('should add text-dark for light Bootstrap colors', () => { + expect(normalizeColor('warning')).toBe('bg-warning text-dark'); + expect(normalizeColor('info')).toBe('bg-info text-dark'); + expect(normalizeColor('light')).toBe('bg-light text-dark'); + }); + + it('should handle case-insensitive Bootstrap color names', () => { + expect(normalizeColor('PRIMARY')).toBe('bg-primary'); + expect(normalizeColor('Success')).toBe('bg-success'); + expect(normalizeColor('DANGER')).toBe('bg-danger'); + expect(normalizeColor('Warning')).toBe('bg-warning text-dark'); + }); + + it('should return colors with bg- prefix as-is', () => { + expect(normalizeColor('bg-primary')).toBe('bg-primary'); + expect(normalizeColor('bg-custom')).toBe('bg-custom'); + expect(normalizeColor('bg-warning text-dark')).toBe('bg-warning text-dark'); + }); + + it('should treat unknown color names as custom colors', () => { + expect(normalizeColor('custom')).toBe('custom'); + expect(normalizeColor('my-color')).toBe('my-color'); + }); + }); + + describe('BADGE_COLOURS constant', () => { + it('should contain all 8 Bootstrap color variants', () => { + expect(BADGE_COLOURS).toHaveLength(8); + expect(BADGE_COLOURS).toContain('bg-primary'); + expect(BADGE_COLOURS).toContain('bg-secondary'); + expect(BADGE_COLOURS).toContain('bg-success'); + expect(BADGE_COLOURS).toContain('bg-danger'); + expect(BADGE_COLOURS).toContain('bg-warning text-dark'); + expect(BADGE_COLOURS).toContain('bg-info text-dark'); + expect(BADGE_COLOURS).toContain('bg-light text-dark'); + expect(BADGE_COLOURS).toContain('bg-dark'); + }); + }); + + describe('MIN_TAGS_FOR_SELECT_ALL constant', () => { + it('should be set to 3', () => { + expect(MIN_TAGS_FOR_SELECT_ALL).toBe(3); + }); + }); +}); diff --git a/packages/vue-components/src/cardstack/Card.vue b/packages/vue-components/src/cardstack/Card.vue index cecc41ef0b..9be1df428f 100644 --- a/packages/vue-components/src/cardstack/Card.vue +++ b/packages/vue-components/src/cardstack/Card.vue @@ -22,7 +22,11 @@ {{ key[0] }} @@ -34,6 +38,7 @@