@abreen/tada 1.6.0 → 1.6.4

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/build/bundle.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  getDistDir,
10
10
  toPosix,
11
11
  } from './utils/paths';
12
- import { deriveTheme } from './utils/derive-theme';
12
+ import { deriveTheme, deriveLinkHue } from './utils/derive-theme';
13
13
  import type { PluginBuilder } from 'bun';
14
14
  import type { SiteVariables } from './types';
15
15
  import timezones from '../src/timezone/timezones.json' with { type: 'json' };
@@ -27,6 +27,12 @@ function renderThemeScss(siteVariables: SiteVariables): string {
27
27
  const iconColorHoverDark = `hsl(${tintHue}deg ${(6 * tintAmount) / 100}% 60%)`;
28
28
  const iconColorTranslucentDark = `hsl(${tintHue}deg ${(85 * tintAmount) / 100}% 90%)`;
29
29
 
30
+ const linkHue = deriveLinkHue(tintHue);
31
+ const linkColor = `hsl(${linkHue}deg 44.4% 49.4%)`;
32
+ const linkColorHover = `hsl(${linkHue}deg 34% 60%)`;
33
+ const linkColorDark = `hsl(${linkHue}deg 50% 72%)`;
34
+ const linkColorHoverDark = `hsl(${linkHue}deg 40% 80%)`;
35
+
30
36
  const rendered = _.template(template)({
31
37
  ...theme,
32
38
  tintHue,
@@ -36,6 +42,10 @@ function renderThemeScss(siteVariables: SiteVariables): string {
36
42
  iconColorDark,
37
43
  iconColorHoverDark,
38
44
  iconColorTranslucentDark,
45
+ linkColor,
46
+ linkColorHover,
47
+ linkColorDark,
48
+ linkColorHoverDark,
39
49
  });
40
50
 
41
51
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tada-'));
@@ -1,5 +1,5 @@
1
1
  import type MarkdownIt from 'markdown-it';
2
- import type Token from 'markdown-it/lib/token.mjs';
2
+ import Token from 'markdown-it/lib/token.mjs';
3
3
  import { makeLogger } from './log';
4
4
  import type { SiteVariables } from './types';
5
5
 
@@ -9,38 +9,197 @@ export default function externalLinks(
9
9
  md: MarkdownIt,
10
10
  siteVariables: SiteVariables,
11
11
  ): void {
12
- function addClass(token: Token): void {
13
- if (token.type === 'link_open') {
14
- const href = token.attrGet('href');
15
- if (!href) {
16
- return;
12
+ function isExternal(href: string): boolean {
13
+ if (!href.match(/^https?:\/\/.*$/)) {
14
+ return false;
15
+ }
16
+ const url = new URL(href);
17
+ return !siteVariables.internalDomains?.includes(url.host);
18
+ }
19
+
20
+ function findMatchingClose(children: Token[], openIdx: number): number {
21
+ let depth = 1;
22
+ for (let i = openIdx + 1; i < children.length; i++) {
23
+ if (children[i].type === 'link_open') {
24
+ depth++;
25
+ } else if (children[i].type === 'link_close') {
26
+ depth--;
27
+ if (depth === 0) {
28
+ return i;
29
+ }
17
30
  }
31
+ }
32
+ return -1;
33
+ }
34
+
35
+ function makeSpanOpen(): Token {
36
+ const t = new Token('html_inline', '', 0);
37
+ t.content = '<span class="external-link-tail">';
38
+ return t;
39
+ }
18
40
 
19
- if (href.match(/^https?:\/\/.*$/)) {
20
- const url = new URL(href);
21
- if (!siteVariables.internalDomains?.includes(url.host)) {
22
- const classAttr = token.attrGet('class');
23
- let newClassAttr;
41
+ function makeSpanClose(): Token {
42
+ const t = new Token('html_inline', '', 0);
43
+ t.content = '</span>';
44
+ return t;
45
+ }
46
+
47
+ // Wrap the trailing word of an external link in
48
+ // <span class="external-link-tail"> so the icon (added via that span's
49
+ // ::after) stays glued to the last word and doesn't wrap on its own.
50
+ // The span is inserted inside any inline-formatting elements (em,
51
+ // strong, code, ...) that are still open at the split point, so the
52
+ // resulting markup stays nested.
53
+ function wrapTail(linkChildren: Token[]): Token[] | null {
54
+ if (linkChildren.length === 0) {
55
+ return null;
56
+ }
24
57
 
25
- if (classAttr) {
26
- newClassAttr = classAttr + ' external';
27
- } else {
28
- newClassAttr = 'external';
29
- }
58
+ // Find the rightmost split char (space or hyphen) in a text token,
59
+ // skipping any candidate that would produce an empty tail (e.g. a
60
+ // trailing hyphen with no content after it).
61
+ let splitTokenIdx = -1;
62
+ let splitCharIdx = -1;
63
+ for (let i = linkChildren.length - 1; i >= 0; i--) {
64
+ const tok = linkChildren[i];
65
+ if (tok.type !== 'text') {
66
+ continue;
67
+ }
68
+ const text = tok.content;
69
+ const hasLaterTokens = i < linkChildren.length - 1;
70
+ let foundIdx = -1;
71
+ for (let j = text.length - 1; j >= 0; j--) {
72
+ const c = text[j];
73
+ if (c !== ' ' && c !== '-') {
74
+ continue;
75
+ }
76
+ if (j < text.length - 1 || hasLaterTokens) {
77
+ foundIdx = j;
78
+ break;
79
+ }
80
+ }
81
+ if (foundIdx !== -1) {
82
+ splitTokenIdx = i;
83
+ splitCharIdx = foundIdx;
84
+ break;
85
+ }
86
+ }
30
87
 
31
- log.debug`${href} -> "${newClassAttr}"`;
32
- token.attrSet('class', newClassAttr);
88
+ if (splitTokenIdx === -1) {
89
+ // No space anywhere in the link's text. Wrap the whole content.
90
+ return [makeSpanOpen(), ...linkChildren, makeSpanClose()];
91
+ }
33
92
 
34
- log.debug`${href} -> target="_blank"`;
35
- token.attrSet('target', '_blank');
93
+ // Find where to put the span close: walk forward from the split
94
+ // point tracking nesting depth (locally opened elements after the
95
+ // split). The span must close before any -1 token that would close
96
+ // an element that was already open at the split point.
97
+ let spanCloseIdx = linkChildren.length;
98
+ let depth = 0;
99
+ for (let i = splitTokenIdx + 1; i < linkChildren.length; i++) {
100
+ const tok = linkChildren[i];
101
+ if (tok.nesting === 1) {
102
+ depth++;
103
+ } else if (tok.nesting === -1) {
104
+ if (depth > 0) {
105
+ depth--;
106
+ } else {
107
+ spanCloseIdx = i;
108
+ break;
36
109
  }
37
110
  }
38
111
  }
39
112
 
40
- token.children?.map(addClass);
113
+ const splitToken = linkChildren[splitTokenIdx];
114
+ const before = splitToken.content.slice(0, splitCharIdx + 1);
115
+ const after = splitToken.content.slice(splitCharIdx + 1);
116
+
117
+ const result: Token[] = [];
118
+
119
+ // Tokens before the split, unchanged.
120
+ for (let i = 0; i < splitTokenIdx; i++) {
121
+ result.push(linkChildren[i]);
122
+ }
123
+
124
+ // The "before" part of the split text token.
125
+ if (before.length > 0) {
126
+ const beforeToken = new Token('text', '', 0);
127
+ beforeToken.content = before;
128
+ result.push(beforeToken);
129
+ }
130
+
131
+ result.push(makeSpanOpen());
132
+
133
+ // The "after" part of the split text token.
134
+ if (after.length > 0) {
135
+ const afterToken = new Token('text', '', 0);
136
+ afterToken.content = after;
137
+ result.push(afterToken);
138
+ }
139
+
140
+ // Tokens between the split and the span close.
141
+ for (let i = splitTokenIdx + 1; i < spanCloseIdx; i++) {
142
+ result.push(linkChildren[i]);
143
+ }
144
+
145
+ result.push(makeSpanClose());
146
+
147
+ // Tokens from the span close position to the end.
148
+ for (let i = spanCloseIdx; i < linkChildren.length; i++) {
149
+ result.push(linkChildren[i]);
150
+ }
151
+
152
+ return result;
153
+ }
154
+
155
+ function processChildren(children: Token[]): Token[] {
156
+ const result: Token[] = [];
157
+ let i = 0;
158
+ while (i < children.length) {
159
+ const token = children[i];
160
+
161
+ if (token.type !== 'link_open') {
162
+ result.push(token);
163
+ i++;
164
+ continue;
165
+ }
166
+
167
+ const href = token.attrGet('href');
168
+ if (!href || !isExternal(href)) {
169
+ result.push(token);
170
+ i++;
171
+ continue;
172
+ }
173
+
174
+ const classAttr = token.attrGet('class');
175
+ const newClassAttr = classAttr ? `${classAttr} external` : 'external';
176
+ token.attrSet('class', newClassAttr);
177
+ token.attrSet('target', '_blank');
178
+ log.debug`${href} -> "${newClassAttr}"`;
179
+
180
+ const closeIdx = findMatchingClose(children, i);
181
+ if (closeIdx === -1) {
182
+ result.push(token);
183
+ i++;
184
+ continue;
185
+ }
186
+
187
+ const linkContent = children.slice(i + 1, closeIdx);
188
+ const wrapped = wrapTail(linkContent);
189
+
190
+ result.push(token);
191
+ result.push(...(wrapped ?? linkContent));
192
+ result.push(children[closeIdx]);
193
+ i = closeIdx + 1;
194
+ }
195
+ return result;
41
196
  }
42
197
 
43
198
  md.core.ruler.push('external_links', state => {
44
- state.tokens.map(addClass);
199
+ state.tokens.forEach(token => {
200
+ if (token.children) {
201
+ token.children = processChildren(token.children);
202
+ }
203
+ });
45
204
  });
46
205
  }
@@ -14,6 +14,9 @@ const LIGHT_TEXT_L_MAX = 0.5;
14
14
  const DARK_TEXT_L_MIN = 0.7;
15
15
  const DARK_TEXT_L_MAX = 0.82;
16
16
 
17
+ const LINK_ANCHOR_HUE = 212;
18
+ const LINK_TINT_BLEND = 0.05;
19
+
17
20
  function clamp(v: number, min: number, max: number): number {
18
21
  return Math.min(Math.max(v, min), max);
19
22
  }
@@ -75,6 +78,11 @@ export function deriveTheme(cssColor: string): DerivedTheme {
75
78
  };
76
79
  }
77
80
 
81
+ export function deriveLinkHue(tintHue: number): number {
82
+ const diff = ((tintHue - LINK_ANCHOR_HUE + 540) % 360) - 180;
83
+ return (LINK_ANCHOR_HUE + diff * LINK_TINT_BLEND + 360) % 360;
84
+ }
85
+
78
86
  export function getTextOnColor(cssColor: string): '#fff' | '#000' {
79
87
  const parsed = parse(cssColor);
80
88
  if (!parsed) {
@@ -36,6 +36,22 @@ function capitalize(str: string): string {
36
36
  return str[0].toUpperCase() + str.slice(1);
37
37
  }
38
38
 
39
+ const MAX_FOOTNOTES = 35;
40
+
41
+ export function footnoteLabel(oneBasedIndex: number): string {
42
+ if (oneBasedIndex < 1 || oneBasedIndex > MAX_FOOTNOTES) {
43
+ throw new Error(
44
+ `Tada supports at most ${MAX_FOOTNOTES} footnotes per page ` +
45
+ `(9 digits + 26 capital letters), but footnote ${oneBasedIndex} ` +
46
+ `was requested. Reduce the number of footnotes on this page.`,
47
+ );
48
+ }
49
+ if (oneBasedIndex <= 9) {
50
+ return String(oneBasedIndex);
51
+ }
52
+ return String.fromCharCode('A'.charCodeAt(0) + (oneBasedIndex - 10));
53
+ }
54
+
39
55
  export function createMarkdown(
40
56
  siteVariables: SiteVariables,
41
57
  options: CreateMarkdownOptions = {},
@@ -196,24 +212,22 @@ export function createMarkdown(
196
212
  });
197
213
 
198
214
  /*
199
- * Customize markdown-it-footnote renderer
215
+ * Customize markdown-it-footnote renderer.
216
+ *
217
+ * Footnotes are labeled with single characters from Inter's ss06
218
+ * stylistic set: 1-9 for the first nine footnotes, then A-Z. The
219
+ * default <ol> numbering is suppressed via CSS and we inject a
220
+ * marker span per item so the label sequence can mix digits and
221
+ * letters.
200
222
  */
201
223
  markdown.renderer.rules.footnote_block_open = () =>
202
224
  '<div class="footnotes"><p class="title">Footnotes</p><ol>';
203
225
 
204
226
  markdown.renderer.rules.footnote_block_close = () => '</ol></div>';
205
227
 
206
- // Change appearance of reference
207
- const caption = markdown.renderer.rules.footnote_caption!;
208
- markdown.renderer.rules.footnote_caption = (
209
- tokens: Token[],
210
- idx: number,
211
- options: Options,
212
- env: unknown,
213
- self: Renderer,
214
- ) => {
215
- const str = caption(tokens, idx, options, env, self);
216
- return str.slice(1, str.length - 1);
228
+ markdown.renderer.rules.footnote_caption = (tokens: Token[], idx: number) => {
229
+ const id = tokens[idx].meta!.id as number;
230
+ return footnoteLabel(id + 1);
217
231
  };
218
232
 
219
233
  const footnoteRef = markdown.renderer.rules.footnote_ref!;
@@ -224,11 +238,26 @@ export function createMarkdown(
224
238
  env: unknown,
225
239
  self: Renderer,
226
240
  ) =>
241
+ // Prepend a non-breaking space so the reference link cannot wrap
242
+ // away from the word it follows.
243
+ '\u00a0' +
227
244
  footnoteRef(tokens, idx, options, env, self)
228
245
  .replace('<sup class="footnote-ref">', '')
229
246
  .replace('</sup>', '')
230
247
  .replace('<a href="', '<a class="footnote-ref" href="');
231
248
 
249
+ markdown.renderer.rules.footnote_open = (tokens: Token[], idx: number) => {
250
+ const id = tokens[idx].meta!.id as number;
251
+ const label = footnoteLabel(id + 1);
252
+ const refid = id + 1;
253
+ return (
254
+ `<li id="fn${refid}" class="footnote-item">` +
255
+ `<span class="footnote-marker" aria-hidden="true">${label}</span> `
256
+ );
257
+ };
258
+
259
+ markdown.renderer.rules.footnote_close = () => '</li>\n';
260
+
232
261
  const footnoteAnchor = markdown.renderer.rules.footnote_anchor!;
233
262
  markdown.renderer.rules.footnote_anchor = (
234
263
  tokens: Token[],
@@ -46,12 +46,17 @@ const log = makeLogger(import.meta.url);
46
46
 
47
47
  const REQUIRED_FRONT_MATTER_FIELDS = ['title'];
48
48
 
49
+ function isWatchMode(assetFiles: string[]): boolean {
50
+ return assetFiles.some(f => f.includes('watch-reload-client'));
51
+ }
52
+
49
53
  interface TemplateParametersInput {
50
54
  pageVariables: Record<string, unknown>;
51
55
  siteVariables: SiteVariables;
52
56
  content: string | null;
53
57
  applyBasePath: (subPath: string) => string;
54
58
  subPath: string;
59
+ isWatchMode: boolean;
55
60
  }
56
61
 
57
62
  function resolveAuthor(
@@ -101,6 +106,7 @@ function createTemplateParameters({
101
106
  content,
102
107
  applyBasePath,
103
108
  subPath,
109
+ isWatchMode,
104
110
  }: TemplateParametersInput): Record<string, unknown> {
105
111
  return {
106
112
  vars: siteVariables.vars || {},
@@ -111,6 +117,7 @@ function createTemplateParameters({
111
117
  page: pageVariables,
112
118
  content,
113
119
  applyBasePath,
120
+ isWatchMode,
114
121
  };
115
122
  }
116
123
 
@@ -188,12 +195,14 @@ export function renderPlainTextPageAsset({
188
195
  const applyBasePath = createApplyBasePath(siteVariables);
189
196
 
190
197
  log.info`Rendering page ${B`${subPath + ext}`}`;
198
+ const watchMode = isWatchMode(assetFiles);
191
199
  const { content, pageVariables, tocItems, alertIds } = renderPlainTextContent(
192
200
  filePath,
193
201
  subPath,
194
202
  siteVariables,
195
203
  applyBasePath,
196
204
  validInternalTargets,
205
+ watchMode,
197
206
  {
198
207
  validateInternalLinks: extensionIsMarkdown(ext.toLowerCase()),
199
208
  traceCache,
@@ -223,6 +232,7 @@ export function renderPlainTextPageAsset({
223
232
  content,
224
233
  applyBasePath,
225
234
  subPath,
235
+ isWatchMode: watchMode,
226
236
  });
227
237
 
228
238
  const templateHtml = render(
@@ -286,6 +296,7 @@ export function renderCodePageAsset({
286
296
  content,
287
297
  applyBasePath,
288
298
  subPath,
299
+ isWatchMode: isWatchMode(assetFiles),
289
300
  });
290
301
 
291
302
  const html = injectAssetTags(
@@ -339,6 +350,7 @@ function renderPlainTextContent(
339
350
  siteVariables: SiteVariables,
340
351
  applyBasePath: (subPath: string) => string,
341
352
  validInternalTargets: Set<string>,
353
+ isWatchMode: boolean,
342
354
  {
343
355
  validateInternalLinks = true,
344
356
  traceCache,
@@ -396,6 +408,7 @@ function renderPlainTextContent(
396
408
  content: null,
397
409
  applyBasePath,
398
410
  subPath,
411
+ isWatchMode,
399
412
  });
400
413
  const pageVariablesProcessed: Record<string, unknown> = Object.entries(
401
414
  pageVariables,
@@ -446,6 +459,7 @@ function renderPlainTextContent(
446
459
  content: strippedContent,
447
460
  applyBasePath,
448
461
  subPath,
462
+ isWatchMode,
449
463
  });
450
464
 
451
465
  if (traceCache && contentDir && distDir) {
@@ -649,6 +663,7 @@ export function renderLiterateJavaPageAsset({
649
663
  content: contentHtml,
650
664
  applyBasePath,
651
665
  subPath,
666
+ isWatchMode: isWatchMode(assetFiles),
652
667
  });
653
668
 
654
669
  const templateHtml = render('literate.html', templateParameters) as string;
@@ -182,10 +182,10 @@ footnote.[^example-footnote]
182
182
  Definition lists are supported using the same syntax as Presto:
183
183
 
184
184
  Stack
185
- : Last-in first-out collection.
185
+ : Last-in first-out collection.[^footnote-2]
186
186
 
187
187
  Queue
188
- : First-in first-out collection.
188
+ : First-in first-out collection.[^footnote-3]
189
189
 
190
190
  Binary tree
191
191
  : Hierarchical structure where each node has up to two children.
@@ -417,7 +417,15 @@ Here's a time range:
417
417
 
418
418
 
419
419
  [^example-footnote]: Footnotes render inside a numbered list at the bottom of
420
- the page.
420
+ the page.
421
+
422
+ Here's a second paragraph.
423
+
424
+ Here's a third paragraph.
425
+
426
+ [^footnote-2]: Also known as "LIFO".
427
+
428
+ [^footnote-3]: Also known as "FIFO".
421
429
 
422
430
  [front-matter]: https://www.npmjs.com/package/front-matter
423
431
  [markdown-it]: https://markdown-it.github.io/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.6.0",
3
+ "version": "1.6.4",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
package/src/_alerts.scss CHANGED
@@ -39,10 +39,15 @@
39
39
  }
40
40
 
41
41
  a {
42
- text-decoration-color: var(--fg-color);
42
+ @include link-underline;
43
+ color: var(--fg-color);
43
44
 
44
45
  &:hover {
45
- text-decoration-color: inherit;
46
+ color: var(--fg2-color);
47
+ }
48
+
49
+ &:active {
50
+ color: var(--link-active-color);
46
51
  }
47
52
  }
48
53
 
@@ -60,6 +65,7 @@
60
65
  }
61
66
 
62
67
  .alert .title::before {
68
+ content: '';
63
69
  left: 8px;
64
70
  top: 8px;
65
71
  position: absolute;
@@ -68,12 +74,11 @@
68
74
  }
69
75
 
70
76
  .alert.warning .title::before {
71
- content: var(--icon-warning);
77
+ @include contained-mask(var(--icon-warning));
72
78
  }
73
79
 
74
80
  i.warning {
75
- background: var(--icon-warning);
76
- background-size: contain;
81
+ @include contained-mask(var(--icon-warning));
77
82
  display: inline-block;
78
83
  height: 1em;
79
84
  width: 1em;
@@ -81,7 +86,7 @@ i.warning {
81
86
  }
82
87
 
83
88
  .alert.note .title::before {
84
- content: var(--icon-note);
89
+ @include contained-mask(var(--icon-note));
85
90
  }
86
91
 
87
92
  .alert .title {
package/src/_base.scss CHANGED
@@ -66,22 +66,18 @@ html {
66
66
  }
67
67
 
68
68
  a {
69
- color: var(--fg-color);
70
- text-decoration-color: var(--fg2-color);
71
- text-decoration-thickness: 1.5px;
69
+ color: var(--link-color);
70
+ text-decoration-line: none;
72
71
 
73
72
  @media (hover: hover) {
74
73
  &:hover {
75
- color: var(--fg2-color);
74
+ color: var(--link-color-hover);
75
+ @include link-underline;
76
76
  }
77
77
  }
78
- }
79
78
 
80
- a:active {
81
- color: var(--link-active-color);
82
-
83
- @media (hover: hover) {
84
- text-decoration-color: var(--link-active-color);
79
+ &:active {
80
+ color: var(--link-active-color);
85
81
  }
86
82
  }
87
83
 
@@ -146,6 +142,7 @@ details .content {
146
142
  details summary {
147
143
  &:hover {
148
144
  color: var(--fg2-color);
145
+ @include link-underline;
149
146
  }
150
147
 
151
148
  &:active {
package/src/_layout.scss CHANGED
@@ -156,10 +156,31 @@ body.literate main table {
156
156
  .text {
157
157
  color: var(--fg2-color);
158
158
  text-decoration: underline;
159
- text-decoration-color: var(--fg2-color);
160
159
  text-decoration-thickness: 1px;
161
160
  text-underline-offset: 3px;
162
161
  }
162
+
163
+ &:hover .text {
164
+ color: var(--link-color-hover);
165
+ }
166
+
167
+ &:active .text {
168
+ color: var(--link-active-color);
169
+ }
170
+
171
+ &:active .avatar {
172
+ border-color: var(--link-active-color);
173
+ }
174
+
175
+ &:focus-visible {
176
+ outline: none;
177
+ }
178
+
179
+ &:focus-visible .text,
180
+ &:focus-visible .avatar {
181
+ outline: 2px solid var(--theme-color);
182
+ outline-offset: 2px;
183
+ }
163
184
  }
164
185
 
165
186
  .avatar {
@@ -167,6 +188,7 @@ body.literate main table {
167
188
  height: 24px;
168
189
  vertical-align: middle;
169
190
  margin-left: 4px;
191
+ border: 1px solid transparent;
170
192
  }
171
193
  }
172
194
  }
package/src/_mixins.scss CHANGED
@@ -2,3 +2,44 @@
2
2
  -webkit-user-select: $val;
3
3
  user-select: $val;
4
4
  }
5
+
6
+ @mixin link-underline {
7
+ text-decoration-line: underline;
8
+ text-decoration-color: currentColor;
9
+ text-decoration-thickness: 1px;
10
+ text-underline-offset: 0.11rem;
11
+ }
12
+
13
+ @mixin subdued-link {
14
+ color: var(--fg-color);
15
+ text-decoration-line: none;
16
+
17
+ @media (hover: hover) {
18
+ &:hover {
19
+ color: var(--fg2-color);
20
+ @include link-underline;
21
+ }
22
+ }
23
+
24
+ &:active {
25
+ color: var(--link-active-color);
26
+ }
27
+ }
28
+
29
+ @mixin contained-icon {
30
+ background-size: contain;
31
+ background-repeat: no-repeat;
32
+ background-position: center center;
33
+ }
34
+
35
+ @mixin contained-mask($url) {
36
+ -webkit-mask-image: $url;
37
+ mask-image: $url;
38
+ -webkit-mask-size: contain;
39
+ mask-size: contain;
40
+ -webkit-mask-repeat: no-repeat;
41
+ mask-repeat: no-repeat;
42
+ -webkit-mask-position: center center;
43
+ mask-position: center center;
44
+ background-color: currentColor;
45
+ }