hexo-design-cards 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ node-version: [18, 20, 22]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ node-version: ${{ matrix.node-version }}
20
+ - run: npm install
21
+ - run: npm test
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-03-01
4
+
5
+ ### Added
6
+ - 8 tag plugins: `banner`, `cards`, `accents`, `compare`, `alert`, `quotes`, `minicards`, `flow`
7
+ - 5 colorways: deep-sea, olive-garden, fiery-ocean, rustic-earth, sunny-beach
8
+ - Per-post colorway override via front matter
9
+ - Optional font size parameter for cards, accents, compare, alert
10
+ - CSS auto-injection via `after_render:html` filter
11
+ - Responsive layout (single-column on mobile)
12
+ - Markdown rendering inside card/accent/compare/alert/mini content
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 leafbird
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # hexo-design-cards
2
+
3
+ [![CI](https://github.com/leafbird/hexo-design-cards/actions/workflows/ci.yml/badge.svg)](https://github.com/leafbird/hexo-design-cards/actions/workflows/ci.yml) [![GitHub release](https://img.shields.io/github/v/release/leafbird/hexo-design-cards)](https://github.com/leafbird/hexo-design-cards/releases) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+
5
+ Beautiful design card tags for [Hexo](https://hexo.io) — replace verbose inline HTML with clean tag syntax.
6
+
7
+ > Flow diagrams, header cards, accent cards, comparison cards, quotes, alerts, mini cards, and section banners with customizable colorways.
8
+
9
+ ## Install
10
+
11
+ ### From GitHub Packages
12
+
13
+ ```bash
14
+ # Add to .npmrc in your Hexo project
15
+ echo "@leafbird:registry=https://npm.pkg.github.com" >> .npmrc
16
+ echo "//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN" >> .npmrc
17
+
18
+ npm install @leafbird/hexo-design-cards
19
+ ```
20
+
21
+ ### From npm (coming soon)
22
+
23
+ ```bash
24
+ npm install hexo-design-cards
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ After installing, use tag syntax in your Hexo markdown files. No additional configuration needed — the plugin auto-injects CSS and uses the **Deep Sea** colorway by default.
30
+
31
+ ## Tags
32
+
33
+ ### Banner
34
+
35
+ Section divider with a bold colored bar.
36
+
37
+ ```markdown
38
+ {% banner "Section 1: Getting Started" %}
39
+ ```
40
+
41
+ ![Banner](screenshots/00-banner.png)
42
+
43
+ ### Cards
44
+
45
+ Grid of cards with colored headers. Great for comparing concepts side by side.
46
+
47
+ ```markdown
48
+ {% cards 2 %}
49
+ {% card "Title A" %}
50
+ Description with **markdown** support.
51
+ `code snippets` work too.
52
+ {% endcard %}
53
+ {% card "Title B" %}
54
+ Another card's content.
55
+ {% endcard %}
56
+ {% endcards %}
57
+ ```
58
+
59
+ - First argument: number of columns (1–4)
60
+ - Optional last argument: font size in px (e.g. `{% cards 2 15 %}`)
61
+
62
+ ![Cards](screenshots/01-cards.png)
63
+
64
+ ### Accent Cards
65
+
66
+ Cards with a colored left border. Perfect for highlighting key points.
67
+
68
+ ```markdown
69
+ {% accents 2 %}
70
+ {% accent "Point 1" %}Description of the first point{% endaccent %}
71
+ {% accent "Point 2" %}Description of the second point{% endaccent %}
72
+ {% accent "Point 3" %}Third point here{% endaccent %}
73
+ {% accent "Point 4" %}Fourth point here{% endaccent %}
74
+ {% endaccents %}
75
+ ```
76
+
77
+ - First argument: number of columns (1–4)
78
+ - Optional last argument: font size in px
79
+
80
+ ![Accent Cards](screenshots/02-accent-cards.png)
81
+
82
+ ### Compare
83
+
84
+ Side-by-side comparison of two options.
85
+
86
+ ```markdown
87
+ {% compare %}
88
+ {% option "Option A" "🔧" %}
89
+ Description of option A.
90
+ {% endoption %}
91
+ {% option "Option B" "🚀" recommended %}
92
+ Description of option B.
93
+ This one is **recommended**.
94
+ {% endoption %}
95
+ {% endcompare %}
96
+ ```
97
+
98
+ - Second argument (optional): emoji
99
+ - `recommended` flag: thicker border for emphasis
100
+ - Optional font size: `{% compare 15 %}`
101
+
102
+ ![Compare](screenshots/03-compare.png)
103
+
104
+ ### Alert
105
+
106
+ Info, warning, or tip box.
107
+
108
+ ```markdown
109
+ {% alert info %}Title|Body text with **markdown**{% endalert %}
110
+ {% alert warning %}Warning title|Warning body{% endalert %}
111
+ {% alert tip %}Tip title|Tip body{% endalert %}
112
+ ```
113
+
114
+ - Types: `info` (ℹ️), `warning` (⚠️), `tip` (💡)
115
+ - Use `|` to separate title and body (optional — omit for no title)
116
+ - Optional font size: `{% alert warning 17 %}`
117
+
118
+ ![Alert](screenshots/04-alert.png)
119
+
120
+ ### Quotes
121
+
122
+ A collection of styled quotes with colored left borders.
123
+
124
+ ```markdown
125
+ {% quotes "Section Title" %}
126
+ {% dcquote "Source 1" %}Quote text here{% enddcquote %}
127
+ {% dcquote "Source 2" %}Another quote{% enddcquote %}
128
+ {% endquotes %}
129
+ ```
130
+
131
+ ![Quotes](screenshots/05-quotes.png)
132
+
133
+ ### Mini Cards
134
+
135
+ Compact cards in a 3-column grid. Good for listing short items.
136
+
137
+ ```markdown
138
+ {% minicards %}
139
+ {% mini "Item A" %}Short description{% endmini %}
140
+ {% mini "Item B" %}Short description{% endmini %}
141
+ {% mini "Item C" %}Short description{% endmini %}
142
+ {% endminicards %}
143
+ ```
144
+
145
+ ![Mini Cards](screenshots/06-mini-cards.png)
146
+
147
+ ### Flow
148
+
149
+ Simple horizontal flow diagram with arrow connectors.
150
+
151
+ ```markdown
152
+ {% flow "Step A|description" "*Step B|description" "Step C|description" %}
153
+ ```
154
+
155
+ - `*` prefix: highlighted step (bold border)
156
+ - `|` inside quotes: separates title and description
157
+ - Trailing `|` after all steps: caption text
158
+
159
+ ```markdown
160
+ {% flow "Request" "*Process" "Response" | Data flow overview %}
161
+ ```
162
+
163
+ ![Flow](screenshots/07-flow-1.png)
164
+ ![Flow](screenshots/07-flow-2.png)
165
+
166
+ ## Font Size Parameter
167
+
168
+ Most container tags accept an optional font size (in px) as the last numeric argument:
169
+
170
+ ```markdown
171
+ {% cards 2 15 %}...{% endcards %} → 2 columns, 15px body text
172
+ {% accents 2 14 %}...{% endaccents %} → 2 columns, 14px body text
173
+ {% compare 16 %}...{% endcompare %} → 16px body text
174
+ {% alert warning 17 %}...{% endalert %} → 17px body text
175
+ ```
176
+
177
+ When omitted, the plugin's CSS defaults apply.
178
+
179
+ ## Colorways
180
+
181
+ Five built-in color palettes. Each has 5 colors (C1–C5) from darkest to lightest.
182
+
183
+ | Colorway | Vibe |
184
+ |----------|------|
185
+ | `deep-sea` (default) | Calm blue-grey |
186
+ | `olive-garden` | Warm olive-gold |
187
+ | `fiery-ocean` | Bold red-blue contrast |
188
+ | `rustic-earth` | Natural earth tones |
189
+ | `sunny-beach` | Vivid orange-teal |
190
+
191
+ Color palettes from [Coolors.co](https://coolors.co).
192
+
193
+ ### Global Setting
194
+
195
+ In your Hexo `_config.yml`:
196
+
197
+ ```yaml
198
+ design_cards:
199
+ colorway: deep-sea
200
+ ```
201
+
202
+ ### Per-Post Override
203
+
204
+ In a post's front matter:
205
+
206
+ ```yaml
207
+ ---
208
+ colorway: fiery-ocean
209
+ ---
210
+ ```
211
+
212
+ ## Responsive
213
+
214
+ All grid layouts collapse to single-column on screens narrower than 768px. Flow diagrams switch to vertical layout.
215
+
216
+ ## License
217
+
218
+ [MIT](LICENSE)
package/index.js ADDED
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ const { getCSS } = require('./lib/css');
4
+
5
+ // Register all tag plugins
6
+ require('./tags/flow')(hexo);
7
+ require('./tags/cards')(hexo);
8
+ require('./tags/accents')(hexo);
9
+ require('./tags/compare')(hexo);
10
+ require('./tags/quotes')(hexo);
11
+ require('./tags/alert')(hexo);
12
+ require('./tags/minicards')(hexo);
13
+ require('./tags/banner')(hexo);
14
+
15
+ // Inject CSS once per page
16
+ hexo.extend.filter.register('after_render:html', function(str) {
17
+ if (str.indexOf('data-hexo-design-cards') > -1) return str;
18
+ if (str.indexOf('dc-') === -1) return str; // no design cards used
19
+ const css = getCSS();
20
+ return str.replace('</head>', css + '\n</head>');
21
+ });
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const colorways = {
4
+ 'deep-sea': {
5
+ name: 'Deep Sea',
6
+ c1: '#0d1b2a', c2: '#1b263b', c3: '#415a77', c4: '#778da9', c5: '#e0e1dd'
7
+ },
8
+ 'olive-garden': {
9
+ name: 'Olive Garden Feast',
10
+ c1: '#283618', c2: '#606c38', c3: '#bc6c25', c4: '#dda15e', c5: '#fefae0'
11
+ },
12
+ 'fiery-ocean': {
13
+ name: 'Fiery Ocean',
14
+ c1: '#780000', c2: '#c1121f', c3: '#003049', c4: '#669bbc', c5: '#fdf0d5'
15
+ },
16
+ 'rustic-earth': {
17
+ name: 'Rustic Earthy Tones',
18
+ c1: '#414833', c2: '#656d4a', c3: '#7f5539', c4: '#a68a64', c5: '#ede0d4'
19
+ },
20
+ 'sunny-beach': {
21
+ name: 'Sunny Beach Day',
22
+ c1: '#001524', c2: '#78290f', c3: '#15616d', c4: '#ff7d00', c5: '#ffecd1'
23
+ }
24
+ };
25
+
26
+ function getColorway(hexo, page) {
27
+ const pageColorway = page && page.colorway;
28
+ const globalColorway = hexo.config.design_cards && hexo.config.design_cards.colorway;
29
+ const key = pageColorway || globalColorway || 'olive-garden';
30
+ return colorways[key] || colorways['olive-garden'];
31
+ }
32
+
33
+ function tint(hex, opacity) {
34
+ opacity = opacity || 0.12;
35
+ const r = parseInt(hex.slice(1,3), 16);
36
+ const g = parseInt(hex.slice(3,5), 16);
37
+ const b = parseInt(hex.slice(5,7), 16);
38
+ return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')';
39
+ }
40
+
41
+ module.exports = { colorways, getColorway, tint };
package/lib/css.js ADDED
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ function getCSS() {
4
+ return `
5
+ <style data-hexo-design-cards>
6
+ .dc-flow{display:flex;align-items:center;justify-content:center;gap:12px;margin:20px 0;flex-wrap:wrap}
7
+ .dc-flow-step{color:#fff;padding:12px 20px;border-radius:8px;text-align:center}
8
+ .dc-flow-step b{display:block}
9
+ .dc-flow-step span{font-size:12px}
10
+ .dc-flow-step.dc-highlight{border-width:3px;border-style:solid}
11
+ .dc-flow-arrow{font-size:24px}
12
+ .dc-flow-caption{text-align:center;font-size:13px;margin:0 0 10px}
13
+
14
+ .dc-cards{display:grid;gap:20px;margin:25px 0}
15
+ .dc-card{border-width:2px;border-style:solid;border-radius:12px;overflow:hidden}
16
+ .dc-card-header{padding:12px 15px}
17
+ .dc-card-header h4{margin:0;color:#fff;font-size:18px}
18
+ .dc-card-body{padding:15px}
19
+ .dc-card-body p{font-size:15px;color:#555;margin:0 0 10px}
20
+ .dc-card-body code{background:var(--dc-c5);padding:2px 6px;border-radius:4px;font-size:13px}
21
+
22
+ .dc-accents{display:grid;gap:15px;margin:25px 0}
23
+ .dc-accent{padding:15px;border-radius:8px;border-left:5px solid}
24
+ .dc-accent b{display:block;margin-bottom:4px}
25
+ .dc-accent p{font-size:15px;margin:5px 0 0;color:#555}
26
+
27
+ .dc-compare{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin:20px 0}
28
+ .dc-compare-option{padding:20px;border-radius:12px;border-width:3px;border-style:solid}
29
+ .dc-compare-option.dc-recommended{border-width:2px}
30
+ .dc-compare-option h4{margin:0 0 10px;text-align:center}
31
+ .dc-compare-option .dc-emoji{text-align:center;font-size:40px}
32
+ .dc-compare-body{text-align:center}
33
+ .dc-compare-body p{font-size:15px;color:#555;line-height:1.6}
34
+ .dc-compare-body p strong{font-size:24px;display:block;margin:10px 0}
35
+
36
+ .dc-quotes{border-width:3px;border-style:solid;border-radius:12px;padding:20px;margin:25px 0}
37
+ .dc-quotes h4{margin:0 0 15px}
38
+ .dc-quote{border-left:3px solid;padding-left:15px;margin-bottom:15px}
39
+ .dc-quote p{margin:0;font-size:14px;font-style:italic}
40
+ .dc-quote cite{display:block;margin:5px 0 0;font-size:12px;font-style:normal}
41
+
42
+ .dc-alert{border-width:2px;border-style:solid;border-radius:12px;padding:20px;margin:25px 0}
43
+ .dc-alert h4{margin:0 0 10px}
44
+ .dc-alert p,.dc-alert div{margin:0;line-height:1.8}
45
+
46
+ .dc-minicards{display:grid;grid-template-columns:repeat(3,1fr);gap:15px;margin:20px 0}
47
+ .dc-mini{border-width:3px;border-style:solid;border-radius:8px;padding:15px}
48
+ .dc-mini h5{margin:0 0 8px}
49
+ .dc-mini p{font-size:13px;color:#555;margin:0}
50
+ .dc-mini code{background:rgba(0,0,0,0.06);padding:1px 5px;border-radius:3px;font-size:0.85em}
51
+
52
+ .dc-banner{color:#fff;padding:15px 20px;border-radius:8px;margin:60px 0 20px}
53
+ .dc-banner h2{margin:0;font-size:20px;font-weight:bold}
54
+
55
+ @media(max-width:768px){
56
+ .dc-cards,.dc-accents{grid-template-columns:1fr!important}
57
+ .dc-compare{grid-template-columns:1fr}
58
+ .dc-minicards{grid-template-columns:1fr}
59
+ .dc-flow{flex-direction:column}
60
+ .dc-flow-arrow{transform:rotate(90deg)}
61
+ }
62
+ </style>`;
63
+ }
64
+
65
+ module.exports = { getCSS };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "hexo-design-cards",
3
+ "version": "0.1.0",
4
+ "description": "Beautiful design card tags for Hexo — flow diagrams, header cards, accent cards, comparison cards, quotes, alerts, mini cards, and section banners with customizable colorways.",
5
+ "main": "index.js",
6
+ "keywords": [
7
+ "hexo",
8
+ "hexo-plugin",
9
+ "hexo-tag",
10
+ "design",
11
+ "cards",
12
+ "colorway",
13
+ "diagram"
14
+ ],
15
+ "author": "leafbird",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/leafbird/hexo-design-cards.git"
20
+ },
21
+ "homepage": "https://github.com/leafbird/hexo-design-cards",
22
+ "directories": {
23
+ "lib": "lib"
24
+ },
25
+ "scripts": {
26
+ "test": "mocha test/"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/leafbird/hexo-design-cards/issues"
30
+ },
31
+ "devDependencies": {
32
+ "mocha": "^11.7.5"
33
+ }
34
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+ const { getColorway, tint } = require('../lib/colorways');
3
+
4
+ module.exports = function(hexo) {
5
+ const colors_cycle = ['c3', 'c1', 'c4', 'c2'];
6
+ let accentIndex = 0;
7
+ let accentFontSize = null;
8
+
9
+ hexo.extend.tag.register('accents', function(args) {
10
+ const cols = parseInt(args[0]) || 2;
11
+ const lastArg = args[args.length - 1];
12
+ accentFontSize = (args.length > 1 && /^\d+$/.test(lastArg)) ? lastArg + 'px' : null;
13
+ accentIndex = 0;
14
+ return '<div class="dc-accents" style="grid-template-columns:repeat(' + cols + ',1fr)">';
15
+ });
16
+
17
+ hexo.extend.tag.register('endaccents', function() {
18
+ return '</div>';
19
+ });
20
+
21
+ hexo.extend.tag.register('accent', function(args, content) {
22
+ const cw = getColorway(hexo, this);
23
+ const title = args.join(' ').replace(/^"|"$/g, '');
24
+ const colorKey = colors_cycle[accentIndex % colors_cycle.length];
25
+ const color = cw[colorKey];
26
+ accentIndex++;
27
+ const rendered = hexo.render.renderSync({ text: content, engine: 'markdown' });
28
+ const fsStyle = accentFontSize ? ' style="font-size:' + accentFontSize + '"' : '';
29
+ return '<div class="dc-accent" style="background:' + tint(color, 0.1) + ';border-left-color:' + color + '">' +
30
+ '<b>' + title + '</b><div' + fsStyle + '>' + rendered + '</div></div>';
31
+ }, { ends: true });
32
+ };
package/tags/alert.js ADDED
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+ const { getColorway, tint } = require('../lib/colorways');
3
+
4
+ module.exports = function(hexo) {
5
+ hexo.extend.tag.register('alert', function(args, content) {
6
+ const cw = getColorway(hexo, this);
7
+ const type = args[0] || 'info';
8
+ // Check if last arg is a number (font size)
9
+ const lastArg = args[args.length - 1];
10
+ const fontSize = (args.length > 1 && /^\d+$/.test(lastArg)) ? lastArg + 'px' : null;
11
+
12
+ const icons = { info: 'ℹ️', warning: '⚠️', tip: '💡' };
13
+ const colorMap = { info: cw.c3, warning: cw.c2, tip: cw.c4 };
14
+ const bgMap = { info: tint(cw.c3, 0.08), warning: tint(cw.c2, 0.08), tip: tint(cw.c4, 0.08) };
15
+ const color = colorMap[type] || cw.c3;
16
+ const bg = bgMap[type] || tint(cw.c3, 0.08);
17
+ const icon = icons[type] || 'ℹ️';
18
+
19
+ const rendered = content.trim();
20
+ const pipeIdx = rendered.indexOf('|');
21
+ let title = '', body = rendered;
22
+ if (pipeIdx > 0 && pipeIdx < 60) {
23
+ title = rendered.slice(0, pipeIdx).trim();
24
+ body = rendered.slice(pipeIdx + 1).trim();
25
+ }
26
+
27
+ const fsStyle = fontSize ? ' style="font-size:' + fontSize + '"' : '';
28
+ let html = '<div class="dc-alert" style="background:' + bg + ';border-color:' + color + '">';
29
+ if (title) html += '<h4 style="color:' + color + '">' + icon + ' ' + title + '</h4>';
30
+ else html += '<h4 style="color:' + color + '">' + icon + '</h4>';
31
+ html += '<div' + fsStyle + '>' + hexo.render.renderSync({ text: body, engine: 'markdown' }) + '</div>';
32
+ html += '</div>';
33
+ return html;
34
+ }, { ends: true });
35
+ };
package/tags/banner.js ADDED
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+ const { getColorway } = require('../lib/colorways');
3
+
4
+ // {% banner "Section N: Title" %}
5
+ module.exports = function(hexo) {
6
+ hexo.extend.tag.register('banner', function(args) {
7
+ const cw = getColorway(hexo, this);
8
+ const text = args.join(' ').replace(/^"|"$/g, '');
9
+ return '<div class="dc-banner" style="background:' + cw.c1 + '"><h2>' + text + '</h2></div>';
10
+ });
11
+ };
package/tags/cards.js ADDED
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+ const { getColorway } = require('../lib/colorways');
3
+
4
+ module.exports = function(hexo) {
5
+ const colors_cycle = ['c3', 'c2', 'c4', 'c1'];
6
+ let cardIndex = 0;
7
+ let cardFontSize = null;
8
+
9
+ hexo.extend.tag.register('cards', function(args) {
10
+ const cols = parseInt(args[0]) || 2;
11
+ const lastArg = args[args.length - 1];
12
+ cardFontSize = (args.length > 1 && /^\d+$/.test(lastArg)) ? lastArg + 'px' : null;
13
+ cardIndex = 0;
14
+ return '<div class="dc-cards" style="grid-template-columns:repeat(' + cols + ',1fr)">';
15
+ });
16
+
17
+ hexo.extend.tag.register('endcards', function() {
18
+ return '</div>';
19
+ });
20
+
21
+ hexo.extend.tag.register('card', function(args, content) {
22
+ const cw = getColorway(hexo, this);
23
+ const title = args.join(' ').replace(/^"|"$/g, '');
24
+ const colorKey = colors_cycle[cardIndex % colors_cycle.length];
25
+ const color = cw[colorKey];
26
+ cardIndex++;
27
+ const rendered = hexo.render.renderSync({ text: content, engine: 'markdown' });
28
+ const fsStyle = cardFontSize ? ' style="font-size:' + cardFontSize + '"' : '';
29
+ return '<div class="dc-card" style="border-color:' + color + '">' +
30
+ '<div class="dc-card-header" style="background:' + color + '"><h4>' + title + '</h4></div>' +
31
+ '<div class="dc-card-body"' + fsStyle + '>' + rendered + '</div></div>';
32
+ }, { ends: true });
33
+ };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+ const { getColorway, tint } = require('../lib/colorways');
3
+
4
+ module.exports = function(hexo) {
5
+ let optionIndex = 0;
6
+ let compareFontSize = null;
7
+
8
+ hexo.extend.tag.register('compare', function(args) {
9
+ optionIndex = 0;
10
+ const lastArg = args[args.length - 1];
11
+ compareFontSize = (args.length > 0 && /^\d+$/.test(lastArg)) ? lastArg + 'px' : null;
12
+ return '<div class="dc-compare">';
13
+ });
14
+
15
+ hexo.extend.tag.register('endcompare', function() {
16
+ return '</div>';
17
+ });
18
+
19
+ hexo.extend.tag.register('option', function(args, content) {
20
+ const cw = getColorway(hexo, this);
21
+ const title = args[0] || '';
22
+ const emoji = args[1] || '';
23
+ const recommended = args.indexOf('recommended') > -1;
24
+ const colors = [cw.c3, cw.c2, cw.c1, cw.c4];
25
+ const color = recommended ? cw.c3 : colors[optionIndex % colors.length];
26
+ const bg = tint(color, 0.08);
27
+ optionIndex++;
28
+ const rendered = hexo.render.renderSync({ text: content, engine: 'markdown' });
29
+ const fsStyle = compareFontSize ? ' style="font-size:' + compareFontSize + '"' : '';
30
+ return '<div class="dc-compare-option' + (recommended ? ' dc-recommended' : '') +
31
+ '" style="background:' + bg + ';border-color:' + color + '">' +
32
+ '<h4 style="color:' + color + '">' + title + '</h4>' +
33
+ (emoji ? '<div class="dc-emoji">' + emoji + '</div>' : '') +
34
+ '<div class="dc-compare-body"' + fsStyle + '>' + rendered + '</div></div>';
35
+ }, { ends: true });
36
+ };
package/tags/flow.js ADDED
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+ const { getColorway } = require('../lib/colorways');
3
+
4
+ // {% flow "Step A|desc" "*Step B|desc" "Step C|desc" %}
5
+ // {% flow "Step A|desc" "*Step B|desc" "Step C|desc" | caption %}
6
+ module.exports = function(hexo) {
7
+ hexo.extend.tag.register('flow', function(args) {
8
+ const cw = getColorway(hexo, this);
9
+
10
+ // Hexo strips quotes and delivers each quoted group as one element in args[].
11
+ // Find caption: look for a bare "|" separator in args
12
+ let caption = '';
13
+ let stepArgs = args;
14
+ const pipeIdx = args.indexOf('|');
15
+ if (pipeIdx > 0) {
16
+ caption = args.slice(pipeIdx + 1).join(' ');
17
+ stepArgs = args.slice(0, pipeIdx);
18
+ }
19
+
20
+ // Each arg is a step: "title|desc" or "*title|desc"
21
+ const steps = stepArgs;
22
+ const colors = [cw.c3, cw.c2, cw.c1, cw.c4];
23
+ let html = '<div class="dc-flow">';
24
+ steps.forEach(function(step, i) {
25
+ if (i > 0) {
26
+ html += '<span class="dc-flow-arrow" style="color:' + cw.c4 + '">→</span>';
27
+ }
28
+ const highlight = step.startsWith('*');
29
+ const text = highlight ? step.slice(1) : step;
30
+ const parts = text.split('|');
31
+ const title = parts[0];
32
+ const desc = parts[1] || '';
33
+ const bg = highlight ? cw.c1 : colors[i % colors.length];
34
+ const borderStyle = highlight ? ';border:3px solid ' + cw.c4 : '';
35
+ html += '<div class="dc-flow-step' + (highlight ? ' dc-highlight' : '') + '" style="background:' + bg + borderStyle + '">';
36
+ html += '<b>' + title + '</b>';
37
+ if (desc) html += '<span>' + desc + '</span>';
38
+ html += '</div>';
39
+ });
40
+ html += '</div>';
41
+ if (caption) {
42
+ html += '<p class="dc-flow-caption" style="color:' + cw.c4 + '">' + caption + '</p>';
43
+ }
44
+ return html;
45
+ });
46
+ };
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+ const { getColorway } = require('../lib/colorways');
3
+
4
+ module.exports = function(hexo) {
5
+ const colors_cycle = ['c1', 'c2', 'c3', 'c4'];
6
+ let miniIndex = 0;
7
+
8
+ hexo.extend.tag.register('minicards', function() {
9
+ miniIndex = 0;
10
+ return '<div class="dc-minicards">';
11
+ });
12
+
13
+ hexo.extend.tag.register('endminicards', function() {
14
+ return '</div>';
15
+ });
16
+
17
+ hexo.extend.tag.register('mini', function(args, content) {
18
+ const cw = getColorway(hexo, this);
19
+ const title = args.join(' ').replace(/^"|"$/g, '');
20
+ const colorKey = colors_cycle[miniIndex % colors_cycle.length];
21
+ miniIndex++;
22
+ const rendered = hexo.render.renderSync({ text: content, engine: 'markdown' });
23
+ return '<div class="dc-mini" style="border-color:' + cw[colorKey] + ';background:' + cw.c5 + '">' +
24
+ '<h5 style="color:' + cw.c2 + '">' + title + '</h5>' +
25
+ rendered + '</div>';
26
+ }, { ends: true });
27
+ };
package/tags/quotes.js ADDED
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+ const { getColorway } = require('../lib/colorways');
3
+
4
+ module.exports = function(hexo) {
5
+ const colors_cycle = ['c3', 'c2', 'c4', 'c1'];
6
+ let quoteIndex = 0;
7
+
8
+ hexo.extend.tag.register('quotes', function(args) {
9
+ const cw = getColorway(hexo, this);
10
+ const title = args.join(' ').replace(/^"|"$/g, '');
11
+ quoteIndex = 0;
12
+ let html = '<div class="dc-quotes" style="background:' + cw.c5 + ';border-color:' + cw.c4 + '">';
13
+ if (title) html += '<h4 style="color:' + cw.c1 + '">' + title + '</h4>';
14
+ return html;
15
+ });
16
+
17
+ hexo.extend.tag.register('endquotes', function() {
18
+ return '</div>';
19
+ });
20
+
21
+ hexo.extend.tag.register('dcquote', function(args, content) {
22
+ const cw = getColorway(hexo, this);
23
+ const source = args.join(' ').replace(/^"|"$/g, '');
24
+ const colorKey = colors_cycle[quoteIndex % colors_cycle.length];
25
+ quoteIndex++;
26
+ return '<div class="dc-quote" style="border-left-color:' + cw[colorKey] + '">' +
27
+ '<p>' + content.trim() + '</p>' +
28
+ '<cite style="color:' + cw.c4 + '">— ' + source + '</cite></div>';
29
+ }, { ends: true });
30
+ };
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Minimal Hexo mock for testing tag plugins.
5
+ */
6
+ function createMockHexo(options) {
7
+ options = options || {};
8
+ const tags = {};
9
+
10
+ const hexo = {
11
+ config: options.config || {},
12
+ extend: {
13
+ tag: {
14
+ register: function(name, fn, opts) {
15
+ tags[name] = { fn: fn, opts: opts || {} };
16
+ }
17
+ },
18
+ filter: {
19
+ register: function() {}
20
+ }
21
+ },
22
+ render: {
23
+ renderSync: function(data) {
24
+ // Simple passthrough — wrap in <p> to mimic markdown
25
+ return '<p>' + (data.text || '').trim() + '</p>';
26
+ }
27
+ }
28
+ };
29
+
30
+ function callTag(name, args, content) {
31
+ const tag = tags[name];
32
+ if (!tag) throw new Error('Tag not registered: ' + name);
33
+ var ctx = options.page || {};
34
+ if (tag.opts.ends) {
35
+ return tag.fn.call(ctx, args, content || '');
36
+ }
37
+ return tag.fn.call(ctx, args);
38
+ }
39
+
40
+ return { hexo: hexo, tags: tags, callTag: callTag };
41
+ }
42
+
43
+ module.exports = { createMockHexo: createMockHexo };
@@ -0,0 +1,228 @@
1
+ 'use strict';
2
+ const assert = require('assert');
3
+ const { createMockHexo } = require('./mock-hexo');
4
+
5
+ describe('hexo-design-cards tags', function() {
6
+
7
+ describe('banner', function() {
8
+ it('should render a banner with title', function() {
9
+ const mock = createMockHexo();
10
+ require('../tags/banner')(mock.hexo);
11
+ const html = mock.callTag('banner', ['Getting Started']);
12
+ assert(html.includes('dc-banner'));
13
+ assert(html.includes('Getting Started'));
14
+ assert(html.includes('<h2>'));
15
+ });
16
+ });
17
+
18
+ describe('cards', function() {
19
+ it('should render cards container with correct columns', function() {
20
+ const mock = createMockHexo();
21
+ require('../tags/cards')(mock.hexo);
22
+ const open = mock.callTag('cards', ['3']);
23
+ assert(open.includes('repeat(3,1fr)'));
24
+ const close = mock.callTag('endcards', []);
25
+ assert.strictEqual(close, '</div>');
26
+ });
27
+
28
+ it('should render a card with title and content', function() {
29
+ const mock = createMockHexo();
30
+ require('../tags/cards')(mock.hexo);
31
+ mock.callTag('cards', ['2']);
32
+ const html = mock.callTag('card', ['Title A'], 'Some content');
33
+ assert(html.includes('dc-card'));
34
+ assert(html.includes('dc-card-header'));
35
+ assert(html.includes('Title A'));
36
+ assert(html.includes('Some content'));
37
+ });
38
+
39
+ it('should apply font size when specified', function() {
40
+ const mock = createMockHexo();
41
+ require('../tags/cards')(mock.hexo);
42
+ mock.callTag('cards', ['2', '14']);
43
+ const html = mock.callTag('card', ['Title'], 'Content');
44
+ assert(html.includes('font-size:14px'));
45
+ });
46
+ });
47
+
48
+ describe('accents', function() {
49
+ it('should render accents container', function() {
50
+ const mock = createMockHexo();
51
+ require('../tags/accents')(mock.hexo);
52
+ const open = mock.callTag('accents', ['2']);
53
+ assert(open.includes('dc-accents'));
54
+ assert(open.includes('repeat(2,1fr)'));
55
+ });
56
+
57
+ it('should render accent card with title', function() {
58
+ const mock = createMockHexo();
59
+ require('../tags/accents')(mock.hexo);
60
+ mock.callTag('accents', ['2']);
61
+ const html = mock.callTag('accent', ['Point 1'], 'Description');
62
+ assert(html.includes('dc-accent'));
63
+ assert(html.includes('Point 1'));
64
+ assert(html.includes('Description'));
65
+ });
66
+ });
67
+
68
+ describe('compare', function() {
69
+ it('should render compare container', function() {
70
+ const mock = createMockHexo();
71
+ require('../tags/compare')(mock.hexo);
72
+ const open = mock.callTag('compare', []);
73
+ assert(open.includes('dc-compare'));
74
+ });
75
+
76
+ it('should render option with emoji and recommended', function() {
77
+ const mock = createMockHexo();
78
+ require('../tags/compare')(mock.hexo);
79
+ const html = mock.callTag('option', ['Option B', '🚀', 'recommended'], 'Best choice');
80
+ assert(html.includes('dc-compare-option'));
81
+ assert(html.includes('🚀'));
82
+ assert(html.includes('Option B'));
83
+ assert(html.includes('Best choice'));
84
+ });
85
+ });
86
+
87
+ describe('alert', function() {
88
+ it('should render info alert', function() {
89
+ const mock = createMockHexo();
90
+ require('../tags/alert')(mock.hexo);
91
+ const html = mock.callTag('alert', ['info'], 'Title|Body text');
92
+ assert(html.includes('dc-alert'));
93
+ assert(html.includes('Title'));
94
+ assert(html.includes('Body text'));
95
+ });
96
+
97
+ it('should render warning alert', function() {
98
+ const mock = createMockHexo();
99
+ require('../tags/alert')(mock.hexo);
100
+ const html = mock.callTag('alert', ['warning'], 'Warn|Be careful');
101
+ assert(html.includes('dc-alert'));
102
+ assert(html.includes('⚠️'));
103
+ });
104
+
105
+ it('should render tip alert', function() {
106
+ const mock = createMockHexo();
107
+ require('../tags/alert')(mock.hexo);
108
+ const html = mock.callTag('alert', ['tip'], 'Tip|Useful info');
109
+ assert(html.includes('dc-alert'));
110
+ assert(html.includes('💡'));
111
+ });
112
+ });
113
+
114
+ describe('quotes', function() {
115
+ it('should render quotes container with title', function() {
116
+ const mock = createMockHexo();
117
+ require('../tags/quotes')(mock.hexo);
118
+ const open = mock.callTag('quotes', ['Famous Quotes']);
119
+ assert(open.includes('dc-quotes'));
120
+ assert(open.includes('Famous Quotes'));
121
+ });
122
+
123
+ it('should render dcquote with source', function() {
124
+ const mock = createMockHexo();
125
+ require('../tags/quotes')(mock.hexo);
126
+ const html = mock.callTag('dcquote', ['Author'], 'Quote text');
127
+ assert(html.includes('dc-quote'));
128
+ assert(html.includes('Author'));
129
+ assert(html.includes('Quote text'));
130
+ });
131
+ });
132
+
133
+ describe('minicards', function() {
134
+ it('should render minicards container', function() {
135
+ const mock = createMockHexo();
136
+ require('../tags/minicards')(mock.hexo);
137
+ const open = mock.callTag('minicards', []);
138
+ assert(open.includes('dc-minicards'));
139
+ });
140
+
141
+ it('should render mini card with title', function() {
142
+ const mock = createMockHexo();
143
+ require('../tags/minicards')(mock.hexo);
144
+ const html = mock.callTag('mini', ['Item A'], 'Short desc');
145
+ assert(html.includes('dc-mini'));
146
+ assert(html.includes('Item A'));
147
+ assert(html.includes('Short desc'));
148
+ });
149
+ });
150
+
151
+ describe('flow', function() {
152
+ it('should render flow steps', function() {
153
+ const mock = createMockHexo();
154
+ require('../tags/flow')(mock.hexo);
155
+ const html = mock.callTag('flow', ['Step A', '*Step B', 'Step C']);
156
+ assert(html.includes('dc-flow'));
157
+ assert(html.includes('dc-flow-step'));
158
+ assert(html.includes('Step A'));
159
+ assert(html.includes('Step B'));
160
+ assert(html.includes('Step C'));
161
+ assert(html.includes('dc-flow-arrow'));
162
+ });
163
+
164
+ it('should highlight step with * prefix', function() {
165
+ const mock = createMockHexo();
166
+ require('../tags/flow')(mock.hexo);
167
+ const html = mock.callTag('flow', ['A', '*B', 'C']);
168
+ assert(html.includes('dc-highlight'));
169
+ // Should not show the * in the output
170
+ assert(!html.includes('>*B<'));
171
+ });
172
+
173
+ it('should render step description with pipe separator', function() {
174
+ const mock = createMockHexo();
175
+ require('../tags/flow')(mock.hexo);
176
+ const html = mock.callTag('flow', ['Draft|Write it', '*Build|hexo generate', 'Deploy|Push']);
177
+ assert(html.includes('Draft'));
178
+ assert(html.includes('Write it'));
179
+ });
180
+
181
+ it('should render caption after bare pipe', function() {
182
+ const mock = createMockHexo();
183
+ require('../tags/flow')(mock.hexo);
184
+ const html = mock.callTag('flow', ['Request', '*Process', 'Response', '|', 'Data flow overview']);
185
+ assert(html.includes('dc-flow-caption'));
186
+ assert(html.includes('Data flow overview'));
187
+ });
188
+ });
189
+
190
+ describe('colorways', function() {
191
+ it('should default to olive-garden', function() {
192
+ const { getColorway } = require('../lib/colorways');
193
+ const hexo = { config: {} };
194
+ const cw = getColorway(hexo, {});
195
+ assert.strictEqual(cw.c1, '#283618');
196
+ });
197
+
198
+ it('should respect page colorway override', function() {
199
+ const { getColorway } = require('../lib/colorways');
200
+ const hexo = { config: {} };
201
+ const cw = getColorway(hexo, { colorway: 'fiery-ocean' });
202
+ assert.strictEqual(cw.c1, '#780000');
203
+ });
204
+
205
+ it('should fall back to olive-garden for unknown colorway', function() {
206
+ const { getColorway } = require('../lib/colorways');
207
+ const hexo = { config: {} };
208
+ const cw = getColorway(hexo, { colorway: 'nonexistent' });
209
+ assert.strictEqual(cw.c1, '#283618');
210
+ });
211
+ });
212
+
213
+ describe('CSS injection', function() {
214
+ it('should generate CSS with all tag classes', function() {
215
+ const { getCSS } = require('../lib/css');
216
+ const css = getCSS();
217
+ assert(css.includes('dc-flow'));
218
+ assert(css.includes('dc-cards'));
219
+ assert(css.includes('dc-accent'));
220
+ assert(css.includes('dc-compare'));
221
+ assert(css.includes('dc-alert'));
222
+ assert(css.includes('dc-quotes'));
223
+ assert(css.includes('dc-mini'));
224
+ assert(css.includes('dc-banner'));
225
+ assert(css.includes('data-hexo-design-cards'));
226
+ });
227
+ });
228
+ });