@abreen/tada 1.19.3 → 1.19.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/README.md CHANGED
@@ -68,6 +68,7 @@ A static site generator. The successor to Presto.
68
68
  * `??? question ... ???` renders a Q&A section; answer is hidden until click
69
69
  - To display clickable multiple choice options, use a [task list][task-list]
70
70
  * `+++ ... +++ ... +++` renders a two-column layout
71
+ * `[Title][Description](url)` renders a two-line superlink
71
72
  * Special heading subtitles with `## Heading # A subtitle here`
72
73
  * `{{{ _partial.md }}}` syntax for including partials
73
74
 
@@ -5,16 +5,23 @@ import type { SiteVariables } from './types';
5
5
 
6
6
  const log = makeLogger(import.meta.url);
7
7
 
8
+ export function isExternalHref(
9
+ href: string,
10
+ siteVariables: SiteVariables,
11
+ ): boolean {
12
+ if (!href.match(/^https?:\/\/.*$/)) {
13
+ return false;
14
+ }
15
+ const url = new URL(href);
16
+ return !siteVariables.internalDomains?.includes(url.host);
17
+ }
18
+
8
19
  export default function externalLinks(
9
20
  md: MarkdownIt,
10
21
  siteVariables: SiteVariables,
11
22
  ): void {
12
23
  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);
24
+ return isExternalHref(href, siteVariables);
18
25
  }
19
26
 
20
27
  function findMatchingClose(children: Token[], openIdx: number): number {
@@ -0,0 +1,146 @@
1
+ import type MarkdownIt from 'markdown-it';
2
+ import type StateInline from 'markdown-it/lib/rules_inline/state_inline.mjs';
3
+ import { isExternalHref } from './external-links-plugin';
4
+ import type { SiteVariables } from './types';
5
+
6
+ const OPEN_BRACKET = 0x5b;
7
+ const CARET = 0x5e;
8
+ const OPEN_PAREN = 0x28;
9
+ const CLOSE_PAREN = 0x29;
10
+ const NEWLINE = 0x0a;
11
+
12
+ // Renders `[Title][Description](/destination.html)` as a two-line superlink.
13
+ // It must run before the built-in `link` rule, which would otherwise
14
+ // treat `[Title][Description]` as a reference link.
15
+ export default function superlinkPlugin(
16
+ md: MarkdownIt,
17
+ siteVariables: SiteVariables,
18
+ ): void {
19
+ function skipSpaces(src: string, pos: number, max: number): number {
20
+ while (pos < max) {
21
+ const code = src.charCodeAt(pos);
22
+ if (!md.utils.isSpace(code) && code !== NEWLINE) {
23
+ break;
24
+ }
25
+ pos++;
26
+ }
27
+ return pos;
28
+ }
29
+
30
+ function pushPart(
31
+ state: StateInline,
32
+ name: 'title' | 'description',
33
+ start: number,
34
+ end: number,
35
+ ): void {
36
+ const open = state.push(`superlink_${name}_open`, 'span', 1);
37
+ open.attrs = [['class', `superlink-${name}`]];
38
+
39
+ const oldPos = state.pos;
40
+ const oldMax = state.posMax;
41
+ state.pos = start;
42
+ state.posMax = end;
43
+ state.md.inline.tokenize(state);
44
+ state.pos = oldPos;
45
+ state.posMax = oldMax;
46
+
47
+ state.push(`superlink_${name}_close`, 'span', -1);
48
+ }
49
+
50
+ function superlinkRule(state: StateInline, silent: boolean): boolean {
51
+ const src = state.src;
52
+ const max = state.posMax;
53
+
54
+ if (src.charCodeAt(state.pos) !== OPEN_BRACKET) {
55
+ return false;
56
+ }
57
+
58
+ // Leave a footnote reference such as `[^1][source](url)` to the footnote
59
+ // plugin, whose rule would otherwise never see it.
60
+ if (src.charCodeAt(state.pos + 1) === CARET) {
61
+ return false;
62
+ }
63
+
64
+ const titleStart = state.pos + 1;
65
+ const titleEnd = state.md.helpers.parseLinkLabel(state, state.pos, true);
66
+ if (titleEnd < 0) {
67
+ return false;
68
+ }
69
+
70
+ // The description label must follow the title label immediately.
71
+ const descriptionOpen = titleEnd + 1;
72
+ if (
73
+ descriptionOpen >= max ||
74
+ src.charCodeAt(descriptionOpen) !== OPEN_BRACKET
75
+ ) {
76
+ return false;
77
+ }
78
+ const descriptionStart = descriptionOpen + 1;
79
+ const descriptionEnd = state.md.helpers.parseLinkLabel(
80
+ state,
81
+ descriptionOpen,
82
+ true,
83
+ );
84
+ if (descriptionEnd < 0) {
85
+ return false;
86
+ }
87
+
88
+ if (
89
+ src.slice(titleStart, titleEnd).trim() === '' ||
90
+ src.slice(descriptionStart, descriptionEnd).trim() === ''
91
+ ) {
92
+ return false;
93
+ }
94
+
95
+ let pos = descriptionEnd + 1;
96
+ if (pos >= max || src.charCodeAt(pos) !== OPEN_PAREN) {
97
+ return false;
98
+ }
99
+ pos = skipSpaces(src, pos + 1, max);
100
+ if (pos >= max) {
101
+ return false;
102
+ }
103
+
104
+ const destination = state.md.helpers.parseLinkDestination(src, pos, max);
105
+ if (!destination.ok) {
106
+ return false;
107
+ }
108
+ const href = state.md.normalizeLink(destination.str);
109
+ if (!state.md.validateLink(href)) {
110
+ return false;
111
+ }
112
+
113
+ pos = skipSpaces(src, destination.pos, max);
114
+ if (pos >= max || src.charCodeAt(pos) !== CLOSE_PAREN) {
115
+ return false;
116
+ }
117
+
118
+ if (!silent) {
119
+ const external = isExternalHref(href, siteVariables);
120
+ const open = state.push('superlink_open', 'a', 1);
121
+ open.attrs = [
122
+ ['href', href],
123
+ ['class', external ? 'button superlink external' : 'button superlink'],
124
+ ];
125
+ if (external) {
126
+ open.attrs.push(['target', '_blank'], ['rel', 'noopener noreferrer']);
127
+ }
128
+
129
+ pushPart(state, 'title', titleStart, titleEnd);
130
+
131
+ // The spans are blocks, so this collapses visually, but it keeps text
132
+ // extraction (search indexing, copy and paste) from gluing the last word
133
+ // of the title to the first word of the description.
134
+ state.push('text', '', 0).content = '\n';
135
+
136
+ pushPart(state, 'description', descriptionStart, descriptionEnd);
137
+
138
+ state.push('superlink_close', 'a', -1);
139
+ }
140
+
141
+ state.pos = pos + 1;
142
+ return true;
143
+ }
144
+
145
+ md.inline.ruler.before('link', 'superlink', superlinkRule);
146
+ }
@@ -12,6 +12,7 @@ import { isBundledLanguage, isPlainTextLanguage } from '../site-variables';
12
12
  import headingSubtitlePlugin from '../heading-subtitle-plugin';
13
13
  import deflistIdPlugin from '../deflist-id-plugin';
14
14
  import externalLinksPlugin from '../external-links-plugin';
15
+ import superlinkPlugin from '../superlink-plugin';
15
16
  import { tocPlugin } from '../toc-plugin';
16
17
  import columnsPlugin from '../columns-plugin';
17
18
  import katexPlugin from './katex';
@@ -61,6 +62,7 @@ export function createMarkdown(
61
62
  .use(markdownItDeflist)
62
63
  .use(deflistIdPlugin)
63
64
  .use(externalLinksPlugin, siteVariables)
65
+ .use(superlinkPlugin, siteVariables)
64
66
  .use(tocPlugin)
65
67
  .use(columnsPlugin)
66
68
  .use(katexPlugin)
@@ -373,6 +373,27 @@ static boolean isOdd(int n) {
373
373
  ```
374
374
  +++
375
375
 
376
+ ### Superlinks
377
+
378
+ Use
379
+
380
+ ```
381
+ [Home][Back to the front page](index.html)
382
+ [Markdown][You are here](markdown.html)
383
+ [KaTeX][The library behind math support](https://katex.org/)
384
+ ```
385
+
386
+ to create a link with a title and a description:
387
+
388
+ [Home][Back to the front page](index.html)
389
+ [Markdown][You are here](markdown.html)
390
+ [KaTeX][The library behind math support](https://katex.org/)
391
+
392
+ Superlinks are rendered as block elements, so don't use them inline, in the
393
+ middle of a paragraph, or in a heading. Put them on their own lines, as above.
394
+ A superlink to another website, like the last one above, opens in a new tab and
395
+ shows an external link icon in place of the chevron.
396
+
376
397
  ---
377
398
 
378
399
  ## Additional features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.19.3",
3
+ "version": "1.19.4",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
package/src/_alerts.scss CHANGED
@@ -35,7 +35,7 @@
35
35
  background: inherit;
36
36
  }
37
37
 
38
- a {
38
+ a:not(.superlink) {
39
39
  @include link-underline;
40
40
 
41
41
  color: var(--fg-color);
package/src/_content.scss CHANGED
@@ -164,6 +164,60 @@ select:focus {
164
164
  border-color: var(--fg-color);
165
165
  }
166
166
 
167
+ a.superlink {
168
+ position: relative;
169
+ display: block;
170
+ padding-block: calc(var(--gap) / 2);
171
+ padding-inline: var(--gap)
172
+ calc(var(--gap) * 2 + var(--icon-breadcrumb-separator-size));
173
+ font-size: inherit;
174
+ text-align: left;
175
+
176
+ + a.superlink {
177
+ margin-top: var(--gap);
178
+ }
179
+
180
+ .superlink-title {
181
+ display: block;
182
+ font-weight: 600;
183
+ }
184
+
185
+ .superlink-description {
186
+ display: block;
187
+ font-weight: normal;
188
+ color: var(--fg2-color);
189
+ }
190
+
191
+ &::after {
192
+ @include contained-mask(var(--icon-breadcrumb-separator));
193
+
194
+ position: absolute;
195
+ top: 50%;
196
+ right: var(--gap);
197
+ width: var(--icon-breadcrumb-separator-size);
198
+ height: var(--icon-breadcrumb-separator-size);
199
+ content: '';
200
+ background-color: var(--fg2-color);
201
+ transform: translateY(-50%);
202
+ }
203
+
204
+ // External superlinks get the external-link icon instead of the chevron from
205
+ // the shared `a.external` rules above, which win on mask, size and content.
206
+ // They also set currentcolor, so restore the subdued color. The icon's box
207
+ // is smaller than the chevron's, so shift it left to share the chevron's
208
+ // center, which keeps the two optically aligned.
209
+ &.external::after {
210
+ right: calc(
211
+ var(--gap) +
212
+ (
213
+ var(--icon-breadcrumb-separator-size) - var(--icon-external-link-size)
214
+ ) /
215
+ 2
216
+ );
217
+ background-color: var(--fg2-color);
218
+ }
219
+ }
220
+
167
221
  button.icon-button {
168
222
  display: inline-flex;
169
223
  align-items: center;