domma-cms 0.14.10 → 0.16.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.
- package/admin/js/lib/effect-defs.js +1 -0
- package/admin/js/lib/effects-builder.js +3 -0
- package/admin/js/lib/markdown-toolbar.js +17 -46
- package/admin/js/templates/effects.html +83 -0
- package/admin/js/views/page-editor.js +12 -12
- package/package.json +2 -2
- package/plugins/analytics/admin/templates/analytics.html +52 -1
- package/plugins/analytics/admin/views/analytics.js +157 -32
- package/plugins/analytics/config.js +10 -2
- package/plugins/analytics/plugin.js +214 -25
- package/plugins/analytics/plugin.json +9 -5
- package/plugins/analytics/public/inject-body.html +25 -7
- package/plugins/blog/admin/templates/blog.html +25 -2
- package/plugins/blog/admin/views/blog.js +72 -56
- package/plugins/blog/admin/views/post-editor.js +98 -79
- package/plugins/blog/plugin.js +133 -0
- package/plugins/blog/plugin.json +3 -3
- package/plugins/blog/templates/post.html +2 -1
- package/plugins/invoice/admin/templates/editor.html +129 -0
- package/plugins/invoice/admin/templates/index.html +43 -0
- package/plugins/invoice/admin/templates/issuers.html +5 -0
- package/plugins/invoice/admin/templates/receivers.html +5 -0
- package/plugins/invoice/admin/views/editor.js +267 -0
- package/plugins/invoice/admin/views/index.js +155 -0
- package/plugins/invoice/admin/views/issuers.js +23 -0
- package/plugins/invoice/admin/views/party-view.js +148 -0
- package/plugins/invoice/admin/views/receivers.js +22 -0
- package/plugins/invoice/collections/invoice-issuers/schema.json +16 -0
- package/plugins/invoice/collections/invoice-receivers/schema.json +15 -0
- package/plugins/invoice/collections/invoices/schema.json +27 -0
- package/plugins/invoice/config.js +16 -0
- package/plugins/invoice/plugin.js +283 -0
- package/plugins/invoice/plugin.json +85 -0
- package/plugins/invoice/templates/invoice-print.html +213 -0
- package/public/js/effects.js +1 -1
- package/server/services/markdown.js +114 -25
- package/server/services/renderer.js +9 -3
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Invoice {{number}}</title>
|
|
6
|
+
<style>
|
|
7
|
+
:root { color-scheme: light; }
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
11
|
+
color: #222;
|
|
12
|
+
background: #f3f3f3;
|
|
13
|
+
margin: 0;
|
|
14
|
+
padding: 24px;
|
|
15
|
+
font-size: 14px;
|
|
16
|
+
line-height: 1.5;
|
|
17
|
+
}
|
|
18
|
+
.invoice {
|
|
19
|
+
max-width: 800px;
|
|
20
|
+
margin: 0 auto;
|
|
21
|
+
background: #fff;
|
|
22
|
+
padding: 48px 56px;
|
|
23
|
+
box-shadow: 0 4px 20px rgba(0,0,0,.08);
|
|
24
|
+
}
|
|
25
|
+
.top {
|
|
26
|
+
display: flex;
|
|
27
|
+
justify-content: space-between;
|
|
28
|
+
align-items: flex-start;
|
|
29
|
+
gap: 24px;
|
|
30
|
+
border-bottom: 2px solid #e5e5e5;
|
|
31
|
+
padding-bottom: 24px;
|
|
32
|
+
margin-bottom: 32px;
|
|
33
|
+
}
|
|
34
|
+
.logo { max-height: 80px; max-width: 240px; display: block; margin-bottom: 12px; }
|
|
35
|
+
.meta { text-align: right; }
|
|
36
|
+
.meta h1 { margin: 0; font-size: 28px; letter-spacing: 1px; color: #000; }
|
|
37
|
+
.meta .number { font-size: 18px; color: #555; margin-top: 4px; }
|
|
38
|
+
.status {
|
|
39
|
+
display: inline-block;
|
|
40
|
+
padding: 4px 10px;
|
|
41
|
+
border-radius: 4px;
|
|
42
|
+
font-size: 11px;
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
letter-spacing: .5px;
|
|
45
|
+
background: #eee;
|
|
46
|
+
color: #444;
|
|
47
|
+
margin-top: 8px;
|
|
48
|
+
}
|
|
49
|
+
.parties {
|
|
50
|
+
display: grid;
|
|
51
|
+
grid-template-columns: 1fr 1fr;
|
|
52
|
+
gap: 32px;
|
|
53
|
+
margin-bottom: 32px;
|
|
54
|
+
}
|
|
55
|
+
.party h3 {
|
|
56
|
+
margin: 0 0 8px;
|
|
57
|
+
font-size: 11px;
|
|
58
|
+
text-transform: uppercase;
|
|
59
|
+
letter-spacing: 1px;
|
|
60
|
+
color: #888;
|
|
61
|
+
}
|
|
62
|
+
.party .name { font-weight: 700; font-size: 15px; margin-bottom: 4px; }
|
|
63
|
+
.party .lines { color: #555; font-size: 13px; }
|
|
64
|
+
table.items {
|
|
65
|
+
width: 100%;
|
|
66
|
+
border-collapse: collapse;
|
|
67
|
+
margin-bottom: 24px;
|
|
68
|
+
}
|
|
69
|
+
table.items th {
|
|
70
|
+
text-align: left;
|
|
71
|
+
font-size: 11px;
|
|
72
|
+
text-transform: uppercase;
|
|
73
|
+
letter-spacing: 1px;
|
|
74
|
+
color: #888;
|
|
75
|
+
padding: 8px 10px;
|
|
76
|
+
border-bottom: 2px solid #e5e5e5;
|
|
77
|
+
}
|
|
78
|
+
table.items td {
|
|
79
|
+
padding: 10px;
|
|
80
|
+
border-bottom: 1px solid #f0f0f0;
|
|
81
|
+
}
|
|
82
|
+
table.items td.num, table.items th.num { text-align: right; }
|
|
83
|
+
.totals {
|
|
84
|
+
width: 320px;
|
|
85
|
+
margin-left: auto;
|
|
86
|
+
margin-bottom: 32px;
|
|
87
|
+
}
|
|
88
|
+
.totals table { width: 100%; border-collapse: collapse; }
|
|
89
|
+
.totals th {
|
|
90
|
+
text-align: left;
|
|
91
|
+
padding: 8px 10px;
|
|
92
|
+
font-weight: 500;
|
|
93
|
+
color: #555;
|
|
94
|
+
}
|
|
95
|
+
.totals td.num {
|
|
96
|
+
text-align: right;
|
|
97
|
+
padding: 8px 10px;
|
|
98
|
+
font-variant-numeric: tabular-nums;
|
|
99
|
+
}
|
|
100
|
+
.totals .grand th, .totals .grand td {
|
|
101
|
+
font-size: 17px;
|
|
102
|
+
font-weight: 700;
|
|
103
|
+
color: #000;
|
|
104
|
+
border-top: 2px solid #222;
|
|
105
|
+
padding-top: 12px;
|
|
106
|
+
}
|
|
107
|
+
.footer {
|
|
108
|
+
margin-top: 32px;
|
|
109
|
+
padding-top: 16px;
|
|
110
|
+
border-top: 1px solid #e5e5e5;
|
|
111
|
+
font-size: 12px;
|
|
112
|
+
color: #888;
|
|
113
|
+
}
|
|
114
|
+
.notes, .bank {
|
|
115
|
+
margin-top: 24px;
|
|
116
|
+
padding: 12px 16px;
|
|
117
|
+
background: #fafafa;
|
|
118
|
+
border-radius: 4px;
|
|
119
|
+
font-size: 13px;
|
|
120
|
+
}
|
|
121
|
+
.muted { color: #999; font-style: italic; text-align: center; }
|
|
122
|
+
.actions {
|
|
123
|
+
max-width: 800px;
|
|
124
|
+
margin: 0 auto 16px;
|
|
125
|
+
display: flex;
|
|
126
|
+
justify-content: flex-end;
|
|
127
|
+
gap: 8px;
|
|
128
|
+
}
|
|
129
|
+
.actions button {
|
|
130
|
+
background: #222;
|
|
131
|
+
color: #fff;
|
|
132
|
+
border: 0;
|
|
133
|
+
padding: 8px 16px;
|
|
134
|
+
font-size: 13px;
|
|
135
|
+
border-radius: 4px;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
}
|
|
138
|
+
@media print {
|
|
139
|
+
body { background: #fff; padding: 0; }
|
|
140
|
+
.invoice { box-shadow: none; padding: 24px; }
|
|
141
|
+
.actions { display: none; }
|
|
142
|
+
}
|
|
143
|
+
</style>
|
|
144
|
+
</head>
|
|
145
|
+
<body>
|
|
146
|
+
|
|
147
|
+
<div class="actions">
|
|
148
|
+
<button onclick="window.print()">Print / Save as PDF</button>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="invoice">
|
|
152
|
+
<div class="top">
|
|
153
|
+
<div>
|
|
154
|
+
{{issuerLogo}}
|
|
155
|
+
<div class="party">
|
|
156
|
+
<div class="name">{{issuerName}}</div>
|
|
157
|
+
<div class="lines">{{issuerAddress}}</div>
|
|
158
|
+
<div class="lines">{{issuerEmail}}</div>
|
|
159
|
+
<div class="lines">{{issuerPhone}}</div>
|
|
160
|
+
{{issuerVat}}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="meta">
|
|
164
|
+
<h1>INVOICE</h1>
|
|
165
|
+
<div class="number">{{number}}</div>
|
|
166
|
+
<div>{{issueDate}}</div>
|
|
167
|
+
{{dueDate}}
|
|
168
|
+
<div class="status">{{status}}</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div class="parties">
|
|
173
|
+
<div class="party">
|
|
174
|
+
<h3>Billed To</h3>
|
|
175
|
+
<div class="name">{{receiverName}}</div>
|
|
176
|
+
<div class="lines">{{receiverAddress}}</div>
|
|
177
|
+
<div class="lines">{{receiverEmail}}</div>
|
|
178
|
+
{{receiverVat}}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<table class="items">
|
|
183
|
+
<thead>
|
|
184
|
+
<tr>
|
|
185
|
+
<th>Description</th>
|
|
186
|
+
<th class="num">Qty</th>
|
|
187
|
+
<th class="num">Unit</th>
|
|
188
|
+
<th class="num">Total</th>
|
|
189
|
+
</tr>
|
|
190
|
+
</thead>
|
|
191
|
+
<tbody>
|
|
192
|
+
{{items}}
|
|
193
|
+
</tbody>
|
|
194
|
+
</table>
|
|
195
|
+
|
|
196
|
+
<div class="totals">
|
|
197
|
+
<table>
|
|
198
|
+
<tr><th>Subtotal</th><td class="num">{{subtotal}}</td></tr>
|
|
199
|
+
{{vatRow}}
|
|
200
|
+
<tr class="grand"><th>Total</th><td class="num">{{total}}</td></tr>
|
|
201
|
+
</table>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{{notes}}
|
|
205
|
+
{{bankDetails}}
|
|
206
|
+
|
|
207
|
+
<div class="footer">{{footerNote}}</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{{autoPrint}}
|
|
211
|
+
|
|
212
|
+
</body>
|
|
213
|
+
</html>
|
package/public/js/effects.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";fetch("/api/effects/settings").then(function(
|
|
1
|
+
(function(){"use strict";fetch("/api/effects/settings").then(function(h){return h.json()}).then(function(h){var A=h.respectMotion!==!1,E=h.defaultDuration||600,z=h.defaultAnimation||"fade",S=h.defaultThreshold!=null?h.defaultThreshold:.1,m=A&&window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches,u=document.querySelector(".page-body");if(!u)return;function r(t,e){return t.getAttribute("data-fx-"+e)}function a(t,e,n){var i=r(t,e);return i!==null?parseFloat(i):n}function y(t,e,n){var i=r(t,e);return i===null?n:i==="true"||i==="1"}function g(t,e,n){var i=new IntersectionObserver(function(o){o[0].isIntersecting&&(i.unobserve(t),n())},{threshold:e});i.observe(t)}u.querySelectorAll(".dm-fx-reveal").forEach(function(t){var e=r(t,"animation")||z,n=a(t,"duration",E),i=a(t,"delay",0),o=a(t,"threshold",S),f=y(t,"once",!0);m||(t.style.opacity="0",t.style.transition="none",e==="slide-up"?t.style.transform="translateY(30px)":e==="slide-down"?t.style.transform="translateY(-30px)":e==="zoom"?t.style.transform="scale(0.85)":e==="flip"&&(t.style.transform="rotateY(90deg)"),requestAnimationFrame(function(){requestAnimationFrame(function(){t.style.transition="opacity "+n+"ms ease "+i+"ms, transform "+n+"ms ease "+i+"ms";var s=new IntersectionObserver(function(c){c.forEach(function(v){v.isIntersecting?(t.style.opacity="1",t.style.transform="",f&&s.unobserve(t)):f||(t.style.opacity="0",e==="slide-up"?t.style.transform="translateY(30px)":e==="slide-down"?t.style.transform="translateY(-30px)":e==="zoom"?t.style.transform="scale(0.85)":e==="flip"?t.style.transform="rotateY(90deg)":t.style.transform="")})},{threshold:o});s.observe(t)})}))}),u.querySelectorAll(".dm-fx-counter").forEach(function(t){var e=a(t,"to",0),n=a(t,"from",0),i=a(t,"duration",2e3),o=r(t,"prefix")||"",f=r(t,"suffix")||"",s=a(t,"decimals",0),c=r(t,"separator")||"";function v(d){var x=d.toFixed(s);if(c){var l=x.split(".");l[0]=l[0].replace(/\B(?=(\d{3})+(?!\d))/g,c),x=l.join(".")}return o+x+f}if(m){t.textContent=v(e);return}g(t,.5,function(){var d=null;requestAnimationFrame(function x(l){d||(d=l);var w=Math.min((l-d)/i,1),q=1-Math.pow(1-w,3);t.textContent=v(n+(e-n)*q),w<1&&requestAnimationFrame(x)})})}),m||u.querySelectorAll(".dm-fx-scramble").forEach(function(t){var e=a(t,"speed",50),n=t.textContent,i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&",o=0;g(t,.3,function(){var f=setInterval(function(){for(var s="",c=0;c<n.length;c++)c<o||n[c]===" "?s+=n[c]:s+=i[Math.floor(Math.random()*i.length)];t.textContent=s,o<n.length?o++:clearInterval(f)},e)})}),m||u.querySelectorAll(".dm-fx-shake").forEach(function(t){var e=a(t,"intensity",5),n=a(t,"duration",500),i=r(t,"direction")||"horizontal",o=i==="vertical"?"translateY":"translateX";t.animate([{transform:o+"(0)"},{transform:o+"(-"+e+"px)"},{transform:o+"("+e+"px)"},{transform:o+"(-"+e+"px)"},{transform:o+"("+e+"px)"},{transform:o+"(0)"}],{duration:n,easing:"ease-in-out"})}),u.querySelectorAll(".dm-fx-ripple").forEach(function(t){t.style.position="relative",t.style.overflow="hidden",t.addEventListener("click",function(e){var n=r(t,"colour")||"rgba(255,255,255,0.3)",i=a(t,"duration",600),o=t.getBoundingClientRect(),f=Math.max(o.width,o.height)*2,s=document.createElement("span");s.style.cssText="position:absolute;border-radius:50%;pointer-events:none;width:"+f+"px;height:"+f+"px;left:"+(e.clientX-o.left-f/2)+"px;top:"+(e.clientY-o.top-f/2)+"px;background:"+n+";transform:scale(0);opacity:1;transition:transform "+i+"ms ease,opacity "+i+"ms ease;",t.appendChild(s),requestAnimationFrame(function(){s.style.transform="scale(1)",s.style.opacity="0"}),setTimeout(function(){s.remove()},i)})});var p=window.Domma&&window.Domma.effects;p&&(u.querySelectorAll(".dm-fx-breathe").forEach(function(t){if(!m){var e={};r(t,"amplitude")&&(e.amplitude=a(t,"amplitude",6)),r(t,"duration")&&(e.duration=a(t,"duration",3e3)),r(t,"easing")&&(e.easing=r(t,"easing")),r(t,"stagger")&&(e.stagger=a(t,"stagger",0)),p.breathe(t,e)}}),u.querySelectorAll(".dm-fx-pulse").forEach(function(t){if(!m){var e={};r(t,"scale")&&(e.scale=a(t,"scale",1.05)),r(t,"duration")&&(e.duration=a(t,"duration",2e3)),r(t,"easing")&&(e.easing=r(t,"easing")),p.pulse(t,e)}}),u.querySelectorAll(".dm-fx-scribe").forEach(function(t){var e={};r(t,"speed")&&(e.speed=a(t,"speed",50)),r(t,"delete-speed")&&(e.deleteSpeed=a(t,"delete-speed",30)),r(t,"cursor")&&(e.cursor=y(t,"cursor",!0)),r(t,"cursor-char")&&(e.cursorChar=r(t,"cursor-char")),r(t,"cursor-type")&&(e.cursorType=r(t,"cursor-type")),r(t,"mode")&&(e.mode=r(t,"mode")),r(t,"loop")&&(e.loop=y(t,"loop",!1)),r(t,"loop-delay")&&(e.loopDelay=a(t,"loop-delay",1e3)),r(t,"pause-on-hover")&&(e.pauseOnHover=y(t,"pause-on-hover",!1));var n=r(t,"actions");if(n)try{e.actions=JSON.parse(n)}catch{return}else{var i=t.textContent.trim();if(!i)return;t.textContent="",e.actions=[{render:i}]}p.scribe(t,e)}),u.querySelectorAll(".dm-fx-twinkle").forEach(function(t){var e={};r(t,"count")&&(e.count=a(t,"count",100)),r(t,"shape")&&(e.shape=r(t,"shape")),r(t,"colour")&&(e.colour=r(t,"colour")),r(t,"min-size")&&(e.minSize=a(t,"min-size",1)),r(t,"max-size")&&(e.maxSize=a(t,"max-size",3)),p.twinkle(t,e)}),typeof p.tickerTape=="function"&&u.querySelectorAll(".dm-fx-ticker-tape").forEach(function(t){if(!m){var e={};r(t,"palette")&&(e.palette=r(t,"palette")),r(t,"density")&&(e.density=a(t,"density",50)),r(t,"speed")&&(e.speed=a(t,"speed",1)),r(t,"sway")&&(e.sway=a(t,"sway",60)),r(t,"rotation-speed")&&(e.rotationSpeed=a(t,"rotation-speed",6)),r(t,"min-width")&&(e.minWidth=a(t,"min-width",5)),r(t,"max-width")&&(e.maxWidth=a(t,"max-width",9)),r(t,"min-height")&&(e.minHeight=a(t,"min-height",12)),r(t,"max-height")&&(e.maxHeight=a(t,"max-height",22)),r(t,"fade-start")&&(e.fadeStart=a(t,"fade-start",.55)),r(t,"burst")&&(e.burst=y(t,"burst",!1)),r(t,"burst-count")&&(e.burstCount=a(t,"burst-count",150)),r(t,"z-index")&&(e.zIndex=a(t,"z-index",1));var n=r(t,"mode");n==="page"?p.tickerTape(null,e):p.tickerTape(t,e)}}));var b=u.querySelectorAll(".dm-fx-celebrate");b.length&&!m&&import("/public/js/celebrations/index.js").then(function(t){var e=t.CelebrationsEffect;b.forEach(function(n){var i=n.getAttribute("data-fx-theme")||"auto",o=n.getAttribute("data-fx-intensity")||"medium",f=n.getAttribute("data-fx-z-index")?parseInt(n.getAttribute("data-fx-z-index"),10):999;if(!(i==="auto"&&(i=e.getCurrentTheme(),!i))){var s={},c=["data-fx-theme","data-fx-intensity","data-fx-z-index"];Array.from(n.attributes).forEach(function(d){d.name.startsWith("data-fx-")&&!c.includes(d.name)&&(s[d.name.slice(8)]=d.value)});var v=new e({theme:i,intensity:o,enabled:!0,zIndex:f,overrides:s});v.init().then(function(){v.start()})}})}).catch(function(){})}).catch(function(){})})();
|
|
@@ -22,7 +22,7 @@ const BUILTIN_SHORTCODES = new Set([
|
|
|
22
22
|
'text', 'button', 'link', 'cta', 'grid', 'row', 'col', 'card',
|
|
23
23
|
'banner', 'slideover', 'counter', 'celebrate', 'firework', 'fireworks', 'scribe',
|
|
24
24
|
'reveal', 'breathe', 'pulse', 'shake', 'scramble', 'ripple', 'twinkle',
|
|
25
|
-
'animate', 'ambient', 'list-group',
|
|
25
|
+
'ticker-tape', 'animate', 'ambient', 'list-group',
|
|
26
26
|
]);
|
|
27
27
|
|
|
28
28
|
// Configure marked for safe output
|
|
@@ -763,7 +763,7 @@ export function scrubCodeRegions(markdown) {
|
|
|
763
763
|
* @param {string} markdown
|
|
764
764
|
* @returns {string}
|
|
765
765
|
*/
|
|
766
|
-
function processPluginShortcodes(markdown) {
|
|
766
|
+
async function processPluginShortcodes(markdown) {
|
|
767
767
|
const processors = getShortcodeProcessors();
|
|
768
768
|
if (!processors.length) return markdown;
|
|
769
769
|
|
|
@@ -771,14 +771,34 @@ function processPluginShortcodes(markdown) {
|
|
|
771
771
|
let result = scrubbed;
|
|
772
772
|
const context = {parseShortcodeAttrs, scrubCodeRegions, marked, processCardBlocks, processGridBlocks, escapeAttr};
|
|
773
773
|
|
|
774
|
+
async function replaceAsync(input, regex, mapMatch) {
|
|
775
|
+
const matches = [];
|
|
776
|
+
input.replace(regex, (...args) => {
|
|
777
|
+
const offset = args[args.length - 2];
|
|
778
|
+
matches.push({ args: args.slice(0, -2), full: args[0], offset });
|
|
779
|
+
return args[0];
|
|
780
|
+
});
|
|
781
|
+
if (!matches.length) return input;
|
|
782
|
+
const out = await Promise.all(matches.map((m) => Promise.resolve(mapMatch(...m.args))));
|
|
783
|
+
let res = '';
|
|
784
|
+
let lastEnd = 0;
|
|
785
|
+
matches.forEach((m, i) => {
|
|
786
|
+
res += input.slice(lastEnd, m.offset) + out[i];
|
|
787
|
+
lastEnd = m.offset + m.full.length;
|
|
788
|
+
});
|
|
789
|
+
return res + input.slice(lastEnd);
|
|
790
|
+
}
|
|
791
|
+
|
|
774
792
|
for (const {name, handler} of processors) {
|
|
775
793
|
// Self-closing: [name attrs /]
|
|
776
|
-
result =
|
|
794
|
+
result = await replaceAsync(
|
|
795
|
+
result,
|
|
777
796
|
new RegExp(`\\[${name}([^\\]]*)\\s*\\/\\]`, 'gi'),
|
|
778
797
|
(_, attrStr) => handler(attrStr, null, context)
|
|
779
798
|
);
|
|
780
799
|
// Wrapping: [name attrs]...[/name]
|
|
781
|
-
result =
|
|
800
|
+
result = await replaceAsync(
|
|
801
|
+
result,
|
|
782
802
|
new RegExp(`\\[${name}([^\\]]*)\\]([\\s\\S]*?)\\[\\/${name}\\]`, 'gi'),
|
|
783
803
|
(_, attrStr, body) => handler(attrStr, body, context)
|
|
784
804
|
);
|
|
@@ -790,7 +810,7 @@ function processPluginShortcodes(markdown) {
|
|
|
790
810
|
* Process built-in Effects shortcodes natively.
|
|
791
811
|
* Handles: [counter /], [celebrate /], [firework], [fireworks], [scribe],
|
|
792
812
|
* [reveal], [breathe], [pulse], [shake], [scramble], [ripple],
|
|
793
|
-
* [twinkle], [animate], [ambient]
|
|
813
|
+
* [twinkle], [ticker-tape], [animate], [ambient]
|
|
794
814
|
*
|
|
795
815
|
* @param {string} markdown
|
|
796
816
|
* @returns {string}
|
|
@@ -902,6 +922,20 @@ function processEffectsBlocks(markdown) {
|
|
|
902
922
|
});
|
|
903
923
|
}
|
|
904
924
|
|
|
925
|
+
// [ticker-tape /] → full-page overlay (mode=page)
|
|
926
|
+
// [ticker-tape] body [/ticker-tape] → container-scoped (mode=container)
|
|
927
|
+
apply('ticker-tape', (attrStr, body) => {
|
|
928
|
+
const attrs = parseShortcodeAttrs(attrStr);
|
|
929
|
+
const dataAttrs = Object.entries(attrs)
|
|
930
|
+
.map(([k, v]) => ` data-fx-${k}="${escapeAttr(String(v))}"`)
|
|
931
|
+
.join('');
|
|
932
|
+
if (body === null) {
|
|
933
|
+
return `<div class="dm-fx-ticker-tape" data-fx-mode="page"${dataAttrs}></div>`;
|
|
934
|
+
}
|
|
935
|
+
const innerHtml = marked.parse(processCardBlocks(processGridBlocks(body.trim())));
|
|
936
|
+
return `<div class="dm-fx-ticker-tape" data-fx-mode="container"${dataAttrs}>${innerHtml}</div>\n`;
|
|
937
|
+
});
|
|
938
|
+
|
|
905
939
|
apply('animate', (attrStr, body) => {
|
|
906
940
|
if (body === null) return '';
|
|
907
941
|
const attrs = parseShortcodeAttrs(attrStr);
|
|
@@ -975,14 +1009,18 @@ function processGridBlocks(markdown) {
|
|
|
975
1009
|
/\[col([^\]]*)\]([\s\S]*?)\[\/col\]/gi,
|
|
976
1010
|
(_, attrStr, body) => {
|
|
977
1011
|
const attrs = parseShortcodeAttrs(attrStr);
|
|
978
|
-
|
|
1012
|
+
const COL_INJECTABLE = ['reveal', 'breathe', 'animate', 'ripple'];
|
|
1013
|
+
const injected = extractInjectedEffects(attrs, COL_INJECTABLE);
|
|
1014
|
+
const baseCls = attrs.span ? `col-span-${attrs.span}` : 'col';
|
|
1015
|
+
const allClasses = [baseCls, ...injected.effectClasses].join(' ');
|
|
1016
|
+
const cls = ` class="${allClasses}"`;
|
|
979
1017
|
const id = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
980
1018
|
// Restore the col body before passing it downstream — otherwise the
|
|
981
1019
|
// scrub placeholders from this processor's store get carried into
|
|
982
1020
|
// processCardBlocks, which creates its own empty store and fails to
|
|
983
1021
|
// decode them, producing the literal string "undefined" in place of
|
|
984
1022
|
// whatever was in <pre>, ``` fences, or `inline code`.
|
|
985
|
-
return `<div${cls}${id}>${marked.parse(processCardBlocks(restore(body.trim())))}</div>`;
|
|
1023
|
+
return `<div${cls}${id}${injected.effectDataAttrs}>${marked.parse(processCardBlocks(restore(body.trim())))}</div>`;
|
|
986
1024
|
}
|
|
987
1025
|
);
|
|
988
1026
|
|
|
@@ -1102,6 +1140,47 @@ export function escapeAttr(str) {
|
|
|
1102
1140
|
.replace(/>/g, '>');
|
|
1103
1141
|
}
|
|
1104
1142
|
|
|
1143
|
+
/**
|
|
1144
|
+
* Extract injected-effect flag attributes from a parsed shortcode attrs object.
|
|
1145
|
+
*
|
|
1146
|
+
* For each effect in `allowed` whose flag attribute is present in `attrs`,
|
|
1147
|
+
* emit a `dm-fx-<effect>` class and convert all `<effect>-<attr>` keys to
|
|
1148
|
+
* `data-fx-<attr>="<escaped-value>"` fragments. Returns the consumed keys
|
|
1149
|
+
* so the host processor can skip them when emitting its own attributes.
|
|
1150
|
+
*
|
|
1151
|
+
* The flag attribute is presence-only (parseShortcodeAttrs stores bare flags
|
|
1152
|
+
* as `true`); only prefixed attributes carry a meaningful value.
|
|
1153
|
+
*
|
|
1154
|
+
* @param {Object<string,string>} attrs - parsed shortcode attribute map
|
|
1155
|
+
* @param {string[]} allowed - effect names this host accepts (e.g. ['reveal','pulse'])
|
|
1156
|
+
* @returns {{effectClasses: string[], effectDataAttrs: string, consumedKeys: Set<string>}}
|
|
1157
|
+
*/
|
|
1158
|
+
export function extractInjectedEffects(attrs, allowed) {
|
|
1159
|
+
const effectClasses = [];
|
|
1160
|
+
const dataAttrParts = [];
|
|
1161
|
+
const consumedKeys = new Set();
|
|
1162
|
+
|
|
1163
|
+
for (const effect of allowed) {
|
|
1164
|
+
if (!(effect in attrs)) continue;
|
|
1165
|
+
effectClasses.push(`dm-fx-${effect}`);
|
|
1166
|
+
consumedKeys.add(effect);
|
|
1167
|
+
|
|
1168
|
+
const prefix = `${effect}-`;
|
|
1169
|
+
for (const key of Object.keys(attrs)) {
|
|
1170
|
+
if (!key.startsWith(prefix)) continue;
|
|
1171
|
+
const param = key.slice(prefix.length);
|
|
1172
|
+
dataAttrParts.push(` data-fx-${param}="${escapeAttr(String(attrs[key]))}"`);
|
|
1173
|
+
consumedKeys.add(key);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
return {
|
|
1178
|
+
effectClasses,
|
|
1179
|
+
effectDataAttrs: dataAttrParts.join(''),
|
|
1180
|
+
consumedKeys,
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1105
1184
|
// ---------------------------------------------------------------------------
|
|
1106
1185
|
// Card shared helpers
|
|
1107
1186
|
// ---------------------------------------------------------------------------
|
|
@@ -1141,6 +1220,10 @@ function cardVariantClasses(attrs) {
|
|
|
1141
1220
|
return classes;
|
|
1142
1221
|
}
|
|
1143
1222
|
|
|
1223
|
+
// Effects that may be injected as flag-attributes onto [card] hosts.
|
|
1224
|
+
// Used by both cardRoot (LAYOUT_RENDERERS path) and renderLegacyCard (fallback).
|
|
1225
|
+
const CARD_INJECTABLE = ['reveal', 'breathe', 'pulse', 'shake', 'twinkle', 'ambient', 'animate', 'ripple'];
|
|
1226
|
+
|
|
1144
1227
|
/**
|
|
1145
1228
|
* Builds the opening `<div>` tag for a card root element.
|
|
1146
1229
|
*
|
|
@@ -1149,10 +1232,11 @@ function cardVariantClasses(attrs) {
|
|
|
1149
1232
|
* @returns {string}
|
|
1150
1233
|
*/
|
|
1151
1234
|
function cardRoot(attrs, extraClasses = []) {
|
|
1152
|
-
const
|
|
1235
|
+
const injected = extractInjectedEffects(attrs, CARD_INJECTABLE);
|
|
1236
|
+
const classes = [...cardVariantClasses(attrs), ...extraClasses, ...injected.effectClasses];
|
|
1153
1237
|
const id = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
1154
1238
|
const coll = attrs.collapsible === 'true' ? ' data-collapsible="true"' : '';
|
|
1155
|
-
return `<div class="${classes.join(' ')}"${id}${coll}>`;
|
|
1239
|
+
return `<div class="${classes.join(' ')}"${id}${coll}${injected.effectDataAttrs}>`;
|
|
1156
1240
|
}
|
|
1157
1241
|
|
|
1158
1242
|
/**
|
|
@@ -1630,12 +1714,15 @@ function renderLegacyCard(attrs, body, markedInstance, escAttr) {
|
|
|
1630
1714
|
const collapsible = attrs.collapsible === 'true';
|
|
1631
1715
|
const variant = strAttr('variant');
|
|
1632
1716
|
|
|
1717
|
+
const injected = extractInjectedEffects(attrs, CARD_INJECTABLE);
|
|
1718
|
+
|
|
1633
1719
|
// Root class list — delegate to the shared helper so variant="gradient",
|
|
1634
1720
|
// gradient="<name>", glass/accent/dark/glow, font, shadow, rounded, etc.
|
|
1635
1721
|
// all work on legacy cards (cards without a `layout` attribute).
|
|
1636
1722
|
const classes = cardVariantClasses(attrs);
|
|
1637
1723
|
// Preserve legacy support for variant="primary" (not handled by cardVariantClasses).
|
|
1638
1724
|
if (variant === 'primary') classes.push('card-primary');
|
|
1725
|
+
classes.push(...injected.effectClasses);
|
|
1639
1726
|
|
|
1640
1727
|
const id = attrs.id ? ` id="${escAttr(attrs.id)}"` : '';
|
|
1641
1728
|
const coll = collapsible ? ' data-collapsible="true"' : '';
|
|
@@ -1692,7 +1779,7 @@ function renderLegacyCard(attrs, body, markedInstance, escAttr) {
|
|
|
1692
1779
|
? `<div class="card-footer">${markedInstance.parse(footerContent)}</div>`
|
|
1693
1780
|
: footer ? `<div class="card-footer">${footer}</div>` : '';
|
|
1694
1781
|
|
|
1695
|
-
return `<div class="${classes.join(' ')}"${coll}${id}>${headerHtml}${bodyHtml}${footerHtml}</div>\n`;
|
|
1782
|
+
return `<div class="${classes.join(' ')}"${coll}${id}${injected.effectDataAttrs}>${headerHtml}${bodyHtml}${footerHtml}</div>\n`;
|
|
1696
1783
|
}
|
|
1697
1784
|
|
|
1698
1785
|
// ---------------------------------------------------------------------------
|
|
@@ -2234,6 +2321,8 @@ function processButtonBlocks(markdown) {
|
|
|
2234
2321
|
|
|
2235
2322
|
function buildButton(attrStr, inner) {
|
|
2236
2323
|
const attrs = parseShortcodeAttrs(attrStr || '');
|
|
2324
|
+
const BUTTON_INJECTABLE = ['breathe', 'pulse', 'shake', 'animate', 'ripple'];
|
|
2325
|
+
const injected = extractInjectedEffects(attrs, BUTTON_INJECTABLE);
|
|
2237
2326
|
const href = attrs.href || '#';
|
|
2238
2327
|
const label = inner !== null ? inner.trim() : (attrs.label || '');
|
|
2239
2328
|
const variant = VALID_VARIANTS.has(attrs.variant) ? attrs.variant : 'primary';
|
|
@@ -2241,11 +2330,12 @@ function processButtonBlocks(markdown) {
|
|
|
2241
2330
|
if (attrs.size === 'sm') classes.push('btn-sm');
|
|
2242
2331
|
if (attrs.size === 'lg') classes.push('btn-lg');
|
|
2243
2332
|
if (attrs.class) classes.push(escapeAttr(attrs.class));
|
|
2333
|
+
classes.push(...injected.effectClasses);
|
|
2244
2334
|
const idAttr = attrs.id ? ` id="${escapeAttr(attrs.id)}"` : '';
|
|
2245
2335
|
const targetAttr = attrs.target ? ` target="${escapeAttr(attrs.target)}"` : '';
|
|
2246
2336
|
const icon = attrs.icon ? `<span data-icon="${escapeAttr(attrs.icon)}"></span> ` : '';
|
|
2247
2337
|
const iconAfter = attrs['icon-after'] ? ` <span data-icon="${escapeAttr(attrs['icon-after'])}"></span>` : '';
|
|
2248
|
-
return `<a href="${escapeAttr(href)}" class="${classes.join(' ')}"${idAttr}${targetAttr}>${icon}${label}${iconAfter}</a>`;
|
|
2338
|
+
return `<a href="${escapeAttr(href)}" class="${classes.join(' ')}"${idAttr}${targetAttr}${injected.effectDataAttrs}>${icon}${label}${iconAfter}</a>`;
|
|
2249
2339
|
}
|
|
2250
2340
|
|
|
2251
2341
|
let result = scrubbed.replace(/\[button([^\]]*?)\/\]/gi, (_, attrStr) => buildButton(attrStr, null));
|
|
@@ -2351,8 +2441,7 @@ function processTableBlocks(markdown) {
|
|
|
2351
2441
|
* fullwidth - "true" breaks out of the page container to span the full viewport width (adds .hero-breakout).
|
|
2352
2442
|
* Must be the literal string "true" — the attribute is otherwise ignored.
|
|
2353
2443
|
* twinkle - Flag attribute — adds particle overlay (requires Effects plugin)
|
|
2354
|
-
* twinkle-
|
|
2355
|
-
* twinkle-colour - Particle colour (CSS value)
|
|
2444
|
+
* twinkle-* - Any prefixed attribute is forwarded as data-fx-* (see extractInjectedEffects)
|
|
2356
2445
|
* blobs - Flag attribute — adds ambient blob background
|
|
2357
2446
|
* blobs-type - Blob animation type (default: "float-blobs")
|
|
2358
2447
|
* class - Extra classes appended to .hero
|
|
@@ -2589,9 +2678,11 @@ function processHeroBlocks(markdown) {
|
|
|
2589
2678
|
const heroColor = attrs.color ? String(attrs.color) : '';
|
|
2590
2679
|
const minHeight = attrs['min-height'] ? String(attrs['min-height']) : '';
|
|
2591
2680
|
|
|
2592
|
-
const
|
|
2593
|
-
const
|
|
2594
|
-
|
|
2681
|
+
const HERO_INJECTABLE = ['reveal', 'breathe', 'pulse', 'twinkle', 'ticker-tape', 'ambient', 'animate'];
|
|
2682
|
+
const injected = extractInjectedEffects(attrs, HERO_INJECTABLE);
|
|
2683
|
+
|
|
2684
|
+
// Legacy ad-hoc 'blobs' flag — keep for backward compat. Different keyspace
|
|
2685
|
+
// (bg-ambient-* CSS class), not folded into the generic helper.
|
|
2595
2686
|
const blobs = 'blobs' in attrs;
|
|
2596
2687
|
const blobsType = attrs['blobs-type'] || 'float-blobs';
|
|
2597
2688
|
|
|
@@ -2603,7 +2694,7 @@ function processHeroBlocks(markdown) {
|
|
|
2603
2694
|
if (overlay) classes.push(`hero-overlay-${overlay}`);
|
|
2604
2695
|
if (fullwidth) classes.push('hero-breakout');
|
|
2605
2696
|
if (cls) classes.push(cls);
|
|
2606
|
-
|
|
2697
|
+
classes.push(...injected.effectClasses);
|
|
2607
2698
|
if (blobs) classes.push(`bg-ambient-${blobsType}`);
|
|
2608
2699
|
|
|
2609
2700
|
const styleParts = [];
|
|
@@ -2616,11 +2707,6 @@ function processHeroBlocks(markdown) {
|
|
|
2616
2707
|
const style = styleParts.length ? ` style="${styleParts.join(';')}"` : '';
|
|
2617
2708
|
const idAttr = id ? ` id="${escapeAttr(id)}"` : '';
|
|
2618
2709
|
|
|
2619
|
-
const twinkleAttrs = twinkle
|
|
2620
|
-
? (twinkleCount ? ` data-fx-count="${escapeAttr(twinkleCount)}"` : '') +
|
|
2621
|
-
(twinkleColour ? ` data-fx-colour="${escapeAttr(twinkleColour)}"` : '')
|
|
2622
|
-
: '';
|
|
2623
|
-
|
|
2624
2710
|
// Button/link/cta must run before marked.parse so their quoted attributes
|
|
2625
2711
|
// aren't HTML-escaped to " — that mangles parseShortcodeAttrs and
|
|
2626
2712
|
// turns `href="/x"` into a bare flag (attrs.href = true → href="true").
|
|
@@ -2634,7 +2720,7 @@ function processHeroBlocks(markdown) {
|
|
|
2634
2720
|
if (processedBody) inner += `<div class="hero-body">${marked.parse(processedBody)}</div>`;
|
|
2635
2721
|
inner += '</div>';
|
|
2636
2722
|
|
|
2637
|
-
return `<div class="${classes.join(' ')}"${idAttr}${style}${
|
|
2723
|
+
return `<div class="${classes.join(' ')}"${idAttr}${style}${injected.effectDataAttrs}>${inner}</div>\n`;
|
|
2638
2724
|
}
|
|
2639
2725
|
);
|
|
2640
2726
|
return restore(result);
|
|
@@ -2678,6 +2764,8 @@ function processBannerBlocks(markdown) {
|
|
|
2678
2764
|
/\[banner([^\]]*)\]([\s\S]*?)\[\/banner\]/gi,
|
|
2679
2765
|
(_, attrStr, body) => {
|
|
2680
2766
|
const attrs = parseShortcodeAttrs(attrStr);
|
|
2767
|
+
const BANNER_INJECTABLE = ['reveal', 'pulse', 'shake', 'ambient', 'animate'];
|
|
2768
|
+
const injected = extractInjectedEffects(attrs, BANNER_INJECTABLE);
|
|
2681
2769
|
const VALID_BANNER_TYPES = new Set(['info', 'success', 'warning', 'danger', 'neutral']);
|
|
2682
2770
|
const type = VALID_BANNER_TYPES.has(attrs.type) ? attrs.type : 'info';
|
|
2683
2771
|
const title = attrs.title || '';
|
|
@@ -2687,6 +2775,7 @@ function processBannerBlocks(markdown) {
|
|
|
2687
2775
|
|
|
2688
2776
|
const classes = [`dm-banner`, `dm-banner--${type}`];
|
|
2689
2777
|
if (extraClass) classes.push(extraClass);
|
|
2778
|
+
classes.push(...injected.effectClasses);
|
|
2690
2779
|
|
|
2691
2780
|
const iconHtml = icon
|
|
2692
2781
|
? `<span class="dm-banner__icon" data-icon="${escapeAttr(icon)}"></span>\n `
|
|
@@ -2700,7 +2789,7 @@ function processBannerBlocks(markdown) {
|
|
|
2700
2789
|
: '';
|
|
2701
2790
|
|
|
2702
2791
|
return (
|
|
2703
|
-
`<div class="${classes.join(' ')}">\n` +
|
|
2792
|
+
`<div class="${classes.join(' ')}"${injected.effectDataAttrs}>\n` +
|
|
2704
2793
|
` ${iconHtml}<div class="dm-banner__body">\n` +
|
|
2705
2794
|
` ${titleHtml}${bodyHtml}` +
|
|
2706
2795
|
` </div>${dismissHtml}\n` +
|
|
@@ -2832,7 +2921,7 @@ export async function parseMarkdown(raw) {
|
|
|
2832
2921
|
const withStaticBlock = await processStaticBlocks(withView);
|
|
2833
2922
|
const withDconfig = processDConfigBlocks(withStaticBlock);
|
|
2834
2923
|
const withEffects = processEffectsBlocks(withDconfig);
|
|
2835
|
-
const withPluginShortcodes = processPluginShortcodes(withEffects);
|
|
2924
|
+
const withPluginShortcodes = await processPluginShortcodes(withEffects);
|
|
2836
2925
|
const withTabs = processTabsBlocks(withPluginShortcodes);
|
|
2837
2926
|
const withAccordion = processAccordionBlocks(withTabs);
|
|
2838
2927
|
const withCarousel = processCarouselBlocks(withAccordion);
|
|
@@ -312,11 +312,17 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
|
|
|
312
312
|
const seoDescription = escapeHtml(seoMeta.description ?? site.seo?.defaultDescription ?? '');
|
|
313
313
|
const ogImage = escapeHtml(seoMeta.ogImage ?? '');
|
|
314
314
|
|
|
315
|
+
const ogType = escapeHtml(seoMeta.ogType ?? 'website');
|
|
315
316
|
const ogTags = [
|
|
316
317
|
`<meta property="og:title" content="${seoTitle}">`,
|
|
317
318
|
`<meta property="og:description" content="${seoDescription}">`,
|
|
318
|
-
`<meta property="og:
|
|
319
|
-
|
|
319
|
+
`<meta property="og:type" content="${ogType}">`,
|
|
320
|
+
ogImage ? `<meta property="og:image" content="${ogImage}">` : '',
|
|
321
|
+
`<meta name="twitter:card" content="${ogImage ? 'summary_large_image' : 'summary'}">`,
|
|
322
|
+
`<meta name="twitter:title" content="${seoTitle}">`,
|
|
323
|
+
`<meta name="twitter:description" content="${seoDescription}">`,
|
|
324
|
+
ogImage ? `<meta name="twitter:image" content="${ogImage}">` : ''
|
|
325
|
+
].filter(Boolean).join('\n');
|
|
320
326
|
|
|
321
327
|
const {fontLink, fontOverride} = buildFontVars(site.fontFamily, site.fontSize);
|
|
322
328
|
const fontStyleTag = fontOverride ? `<style>${fontOverride}</style>` : '';
|
|
@@ -363,7 +369,7 @@ export async function renderBlogPage(templatePath, data = {}, seoMeta = {}) {
|
|
|
363
369
|
headInjectLate: [injection.headLate, customCssTag, navbarStyleTag].filter(Boolean).join('\n'),
|
|
364
370
|
bodyEndInject: [
|
|
365
371
|
injection.bodyEnd,
|
|
366
|
-
(
|
|
372
|
+
(seoMeta.usedComponents || [])
|
|
367
373
|
.map(n => `<script type="module" src="/api/components/${n}.js"></script>`)
|
|
368
374
|
.join('\n')
|
|
369
375
|
].filter(Boolean).join('\n'),
|