@geekity/cms 0.18.0 → 0.20.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/README.md +29 -25
- package/admin/components/admin-bar.njk +4 -3
- package/admin/components/public-admin-bar.njk +3 -1
- package/admin/layouts/shell.njk +1 -0
- package/admin/pages/documents/editor.njk +291 -192
- package/admin/static/admin.css +123 -13
- package/admin/static/location.js +38 -0
- package/dist/admin/documents.d.ts +18 -3
- package/dist/admin/documents.d.ts.map +1 -1
- package/dist/admin/documents.js +150 -21
- package/dist/admin/documents.js.map +1 -1
- package/dist/admin/editor-layout.d.ts +13 -0
- package/dist/admin/editor-layout.d.ts.map +1 -0
- package/dist/admin/editor-layout.js +62 -0
- package/dist/admin/editor-layout.js.map +1 -0
- package/dist/admin/enclosure-field.d.ts +2 -3
- package/dist/admin/enclosure-field.d.ts.map +1 -1
- package/dist/admin/enclosure-field.js +27 -7
- package/dist/admin/enclosure-field.js.map +1 -1
- package/dist/admin/headers.d.ts +1 -0
- package/dist/admin/headers.d.ts.map +1 -1
- package/dist/admin/headers.js +3 -0
- package/dist/admin/headers.js.map +1 -1
- package/dist/admin/location-field.d.ts +4 -3
- package/dist/admin/location-field.d.ts.map +1 -1
- package/dist/admin/location-field.js +10 -2
- package/dist/admin/location-field.js.map +1 -1
- package/dist/admin/media.d.ts.map +1 -1
- package/dist/admin/media.js +3 -0
- package/dist/admin/media.js.map +1 -1
- package/dist/admin/photo-field.d.ts +2 -3
- package/dist/admin/photo-field.d.ts.map +1 -1
- package/dist/admin/photo-field.js +4 -2
- package/dist/admin/photo-field.js.map +1 -1
- package/dist/admin/read-field.d.ts +2 -3
- package/dist/admin/read-field.d.ts.map +1 -1
- package/dist/admin/read-field.js +12 -4
- package/dist/admin/read-field.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/content/citation.d.ts +5 -0
- package/dist/content/citation.d.ts.map +1 -1
- package/dist/content/citation.js +17 -0
- package/dist/content/citation.js.map +1 -1
- package/dist/content/kept-properties.d.ts +5 -0
- package/dist/content/kept-properties.d.ts.map +1 -0
- package/dist/content/kept-properties.js +12 -0
- package/dist/content/kept-properties.js.map +1 -0
- package/dist/content/location.d.ts +4 -0
- package/dist/content/location.d.ts.map +1 -1
- package/dist/content/location.js +35 -3
- package/dist/content/location.js.map +1 -1
- package/dist/content/locations.d.ts +2 -5
- package/dist/content/locations.d.ts.map +1 -1
- package/dist/content/locations.js +2 -77
- package/dist/content/locations.js.map +1 -1
- package/dist/content/markdown.d.ts +9 -0
- package/dist/content/markdown.d.ts.map +1 -1
- package/dist/content/markdown.js +37 -1
- package/dist/content/markdown.js.map +1 -1
- package/dist/content/permalink-file.d.ts +7 -0
- package/dist/content/permalink-file.d.ts.map +1 -0
- package/dist/content/permalink-file.js +77 -0
- package/dist/content/permalink-file.js.map +1 -0
- package/dist/content/post-type.d.ts +1 -6
- package/dist/content/post-type.d.ts.map +1 -1
- package/dist/content/post-type.js +13 -14
- package/dist/content/post-type.js.map +1 -1
- package/dist/content/slug.d.ts +2 -1
- package/dist/content/slug.d.ts.map +1 -1
- package/dist/content/slug.js +3 -1
- package/dist/content/slug.js.map +1 -1
- package/dist/env.d.ts +6 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/federation/article.d.ts +8 -7
- package/dist/federation/article.d.ts.map +1 -1
- package/dist/federation/article.js +37 -18
- package/dist/federation/article.js.map +1 -1
- package/dist/federation/citations.d.ts +7 -6
- package/dist/federation/citations.d.ts.map +1 -1
- package/dist/federation/citations.js +44 -28
- package/dist/federation/citations.js.map +1 -1
- package/dist/federation/delivery.d.ts.map +1 -1
- package/dist/federation/delivery.js +23 -16
- package/dist/federation/delivery.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/indieauth/bearer.d.ts +10 -6
- package/dist/indieauth/bearer.d.ts.map +1 -1
- package/dist/indieauth/bearer.js +11 -9
- package/dist/indieauth/bearer.js.map +1 -1
- package/dist/indieauth/token.js +1 -1
- package/dist/indieauth/token.js.map +1 -1
- package/dist/micropub/content.d.ts +15 -0
- package/dist/micropub/content.d.ts.map +1 -0
- package/dist/micropub/content.js +254 -0
- package/dist/micropub/content.js.map +1 -0
- package/dist/micropub/create.d.ts +7 -3
- package/dist/micropub/create.d.ts.map +1 -1
- package/dist/micropub/create.js +75 -24
- package/dist/micropub/create.js.map +1 -1
- package/dist/micropub/endpoint.d.ts +4 -5
- package/dist/micropub/endpoint.d.ts.map +1 -1
- package/dist/micropub/endpoint.js +33 -12
- package/dist/micropub/endpoint.js.map +1 -1
- package/dist/micropub/media.d.ts +6 -4
- package/dist/micropub/media.d.ts.map +1 -1
- package/dist/micropub/media.js +7 -5
- package/dist/micropub/media.js.map +1 -1
- package/dist/micropub/update.d.ts +7 -1
- package/dist/micropub/update.d.ts.map +1 -1
- package/dist/micropub/update.js +32 -7
- package/dist/micropub/update.js.map +1 -1
- package/dist/web/admin-bar.d.ts.map +1 -1
- package/dist/web/admin-bar.js +27 -1
- package/dist/web/admin-bar.js.map +1 -1
- package/dist/web/context.d.ts +10 -3
- package/dist/web/context.d.ts.map +1 -1
- package/dist/web/context.js +12 -2
- package/dist/web/context.js.map +1 -1
- package/dist/web/documents.d.ts +21 -6
- package/dist/web/documents.d.ts.map +1 -1
- package/dist/web/documents.js +23 -12
- package/dist/web/documents.js.map +1 -1
- package/dist/web/feed-item.d.ts +26 -3
- package/dist/web/feed-item.d.ts.map +1 -1
- package/dist/web/feed-item.js +63 -6
- package/dist/web/feed-item.js.map +1 -1
- package/dist/web/feed-rss.js +3 -3
- package/dist/web/feed-rss.js.map +1 -1
- package/dist/web/feed-source.d.ts +7 -0
- package/dist/web/feed-source.d.ts.map +1 -1
- package/dist/web/feed-source.js.map +1 -1
- package/dist/web/feeds.d.ts.map +1 -1
- package/dist/web/feeds.js +7 -1
- package/dist/web/feeds.js.map +1 -1
- package/dist/web/negotiate.d.ts +4 -0
- package/dist/web/negotiate.d.ts.map +1 -1
- package/dist/web/negotiate.js +5 -1
- package/dist/web/negotiate.js.map +1 -1
- package/dist/web/render.d.ts +5 -4
- package/dist/web/render.d.ts.map +1 -1
- package/dist/web/render.js +34 -12
- package/dist/web/render.js.map +1 -1
- package/dist/web/routes.d.ts.map +1 -1
- package/dist/web/routes.js +16 -4
- package/dist/web/routes.js.map +1 -1
- package/dist/web/sanitize.d.ts +10 -0
- package/dist/web/sanitize.d.ts.map +1 -1
- package/dist/web/sanitize.js +180 -0
- package/dist/web/sanitize.js.map +1 -1
- package/dist/web/templates.d.ts.map +1 -1
- package/dist/web/templates.js +2 -0
- package/dist/web/templates.js.map +1 -1
- package/dist/webmention/cited-picture.d.ts +37 -0
- package/dist/webmention/cited-picture.d.ts.map +1 -0
- package/dist/webmention/cited-picture.js +121 -0
- package/dist/webmention/cited-picture.js.map +1 -0
- package/dist/webmention/fetch-public.d.ts +26 -8
- package/dist/webmention/fetch-public.d.ts.map +1 -1
- package/dist/webmention/fetch-public.js +22 -12
- package/dist/webmention/fetch-public.js.map +1 -1
- package/dist/webmention/reply-context.d.ts +30 -8
- package/dist/webmention/reply-context.d.ts.map +1 -1
- package/dist/webmention/reply-context.js +238 -27
- package/dist/webmention/reply-context.js.map +1 -1
- package/dist/webmention/reply-contexts.d.ts +28 -16
- package/dist/webmention/reply-contexts.d.ts.map +1 -1
- package/dist/webmention/reply-contexts.js +108 -25
- package/dist/webmention/reply-contexts.js.map +1 -1
- package/package.json +10 -9
- package/themes/default/README.md +83 -9
- package/themes/default/layouts/post.njk +7 -7
- package/themes/default/partials/citations.njk +25 -4
- package/themes/default/partials/cited-page.njk +58 -0
- package/themes/default/partials/post-list.njk +8 -5
- package/themes/default/partials/reply-context.njk +9 -11
- package/themes/default/src/style.css +37 -0
- package/themes/default/static/style.css +83 -0
package/admin/static/admin.css
CHANGED
|
@@ -490,35 +490,145 @@ button[type='submit']:hover,
|
|
|
490
490
|
min-height: 28rem;
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
-
.admin-editor-
|
|
493
|
+
.admin-editor-group {
|
|
494
494
|
margin: 1.25rem 0 0;
|
|
495
|
-
padding: 0.75rem 1rem 1rem;
|
|
496
495
|
border: 1px solid var(--admin-border);
|
|
497
496
|
}
|
|
498
497
|
|
|
499
|
-
.admin-editor-
|
|
500
|
-
margin:
|
|
501
|
-
padding: 0.75rem 1rem 1rem;
|
|
502
|
-
border: 1px solid var(--admin-border);
|
|
498
|
+
.admin-editor-side > .admin-editor-group:first-child {
|
|
499
|
+
margin-top: 0;
|
|
503
500
|
}
|
|
504
501
|
|
|
505
|
-
.admin-editor-
|
|
506
|
-
|
|
507
|
-
font-
|
|
502
|
+
.admin-editor-group > summary {
|
|
503
|
+
padding: 0.5rem 0.75rem;
|
|
504
|
+
font-weight: 600;
|
|
505
|
+
cursor: pointer;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.admin-editor-group > summary:focus-visible {
|
|
509
|
+
outline: 2px solid var(--admin-link);
|
|
510
|
+
outline-offset: 2px;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.admin-editor-group > fieldset {
|
|
514
|
+
min-width: 0;
|
|
515
|
+
margin: 0;
|
|
516
|
+
padding: 0 0.75rem 0.75rem;
|
|
517
|
+
border: 0;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.admin-editor-group > fieldset > label:first-of-type {
|
|
521
|
+
margin-top: 0.25rem;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.admin-editor-row {
|
|
525
|
+
margin-top: 0.75rem;
|
|
508
526
|
}
|
|
509
527
|
|
|
510
|
-
.admin-editor-
|
|
528
|
+
.admin-editor-row > fieldset {
|
|
511
529
|
display: grid;
|
|
512
530
|
grid-template-columns: max-content minmax(0, 1fr);
|
|
513
531
|
gap: 0.4rem 0.75rem;
|
|
514
532
|
align-items: center;
|
|
515
|
-
|
|
516
|
-
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.admin-editor-row > fieldset > label:first-of-type {
|
|
536
|
+
margin-top: 0;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.admin-editor-row label {
|
|
540
|
+
margin: 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.admin-editor-row .admin-field-error {
|
|
544
|
+
grid-column: 1 / -1;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.admin-cited-card {
|
|
548
|
+
position: relative;
|
|
549
|
+
margin-top: 0.5rem;
|
|
550
|
+
padding: 0.5rem;
|
|
517
551
|
border: 1px solid var(--admin-border);
|
|
552
|
+
background: var(--admin-surface);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.admin-cited-card img {
|
|
556
|
+
display: block;
|
|
557
|
+
width: 100%;
|
|
558
|
+
height: auto;
|
|
559
|
+
max-height: 8rem;
|
|
560
|
+
object-fit: cover;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.admin-cited-title {
|
|
564
|
+
margin: 0.375rem 0 0;
|
|
565
|
+
overflow-wrap: break-word;
|
|
518
566
|
}
|
|
519
567
|
|
|
520
|
-
.admin-
|
|
568
|
+
.admin-cited-card input[type='checkbox'] {
|
|
569
|
+
position: absolute;
|
|
570
|
+
width: 1px;
|
|
571
|
+
height: 1px;
|
|
572
|
+
margin: 0;
|
|
573
|
+
opacity: 0;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.admin-editor-group .admin-cited-remove {
|
|
577
|
+
position: absolute;
|
|
578
|
+
top: 0.75rem;
|
|
579
|
+
inset-inline-end: 0.75rem;
|
|
580
|
+
display: inline-grid;
|
|
581
|
+
place-items: center;
|
|
582
|
+
width: 1.75rem;
|
|
583
|
+
height: 1.75rem;
|
|
521
584
|
margin: 0;
|
|
585
|
+
border: 1px solid var(--admin-border);
|
|
586
|
+
border-radius: 50%;
|
|
587
|
+
background: var(--admin-field);
|
|
588
|
+
font-size: 1.1rem;
|
|
589
|
+
line-height: 1;
|
|
590
|
+
cursor: pointer;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.admin-cited-card input:focus-visible + .admin-cited-remove {
|
|
594
|
+
outline: 2px solid var(--admin-focus);
|
|
595
|
+
outline-offset: 2px;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.admin-cited-card input:checked + .admin-cited-remove {
|
|
599
|
+
background: var(--admin-text);
|
|
600
|
+
color: var(--admin-surface);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.admin-cited-removed {
|
|
604
|
+
display: none;
|
|
605
|
+
margin: 0.25rem 0 0;
|
|
606
|
+
color: var(--admin-muted);
|
|
607
|
+
font-size: 0.9em;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.admin-cited-card:has(input:checked) img,
|
|
611
|
+
.admin-cited-card:has(input:checked) .admin-cited-title {
|
|
612
|
+
opacity: 0.4;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.admin-cited-card:has(input:checked) .admin-cited-removed {
|
|
616
|
+
display: block;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.admin-location-here {
|
|
620
|
+
margin-top: 0.75rem;
|
|
621
|
+
padding: 0.3rem 0.8rem;
|
|
622
|
+
border: 1px solid var(--admin-link);
|
|
623
|
+
border-radius: 3px;
|
|
624
|
+
background: var(--admin-surface);
|
|
625
|
+
color: var(--admin-link);
|
|
626
|
+
font: inherit;
|
|
627
|
+
cursor: pointer;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.admin-location-here:hover {
|
|
631
|
+
background: var(--admin-field);
|
|
522
632
|
}
|
|
523
633
|
|
|
524
634
|
.admin-hint {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
if (!navigator.geolocation) return;
|
|
3
|
+
|
|
4
|
+
var button = document.getElementById('editor-location-here');
|
|
5
|
+
var status = document.getElementById('editor-location-status');
|
|
6
|
+
var geo = document.getElementById('editor-location-geo');
|
|
7
|
+
var accuracy = document.getElementById('editor-location-accuracy');
|
|
8
|
+
if (!button || !status || !geo || !accuracy) return;
|
|
9
|
+
|
|
10
|
+
var problems = {
|
|
11
|
+
1: 'The browser was not allowed to share its location. Type the coordinates instead, or allow location for this site and try again.',
|
|
12
|
+
2: 'The browser could not find its location. Type the coordinates instead.',
|
|
13
|
+
3: 'The browser took too long to find its location. Try again, or type the coordinates instead.',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
button.hidden = false;
|
|
17
|
+
button.addEventListener('click', function () {
|
|
18
|
+
button.disabled = true;
|
|
19
|
+
status.textContent = 'Finding your location…';
|
|
20
|
+
navigator.geolocation.getCurrentPosition(
|
|
21
|
+
function (position) {
|
|
22
|
+
var coords = position.coords;
|
|
23
|
+
geo.value = coords.latitude.toFixed(5) + ', ' + coords.longitude.toFixed(5);
|
|
24
|
+
accuracy.value = String(Math.round(coords.accuracy));
|
|
25
|
+
var block = button.closest('details');
|
|
26
|
+
if (block) block.open = true;
|
|
27
|
+
status.textContent =
|
|
28
|
+
'Filled in from the browser, to within about ' + accuracy.value + ' metres.';
|
|
29
|
+
button.disabled = false;
|
|
30
|
+
},
|
|
31
|
+
function (error) {
|
|
32
|
+
status.textContent = problems[error.code] || problems[2];
|
|
33
|
+
button.disabled = false;
|
|
34
|
+
},
|
|
35
|
+
{ enableHighAccuracy: true, timeout: 20000, maximumAge: 60000 },
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
})();
|
|
@@ -3,6 +3,7 @@ import type { Document, DocumentType } from '../content/document.ts';
|
|
|
3
3
|
import type { Enclosure } from '../content/enclosure.ts';
|
|
4
4
|
import type { Photo } from '../content/photo.ts';
|
|
5
5
|
import type { PostLocation } from '../content/location.ts';
|
|
6
|
+
import type { KeptProperties } from '../content/kept-properties.ts';
|
|
6
7
|
import type { ContentStore, ListAllOptions } from '../content/store.ts';
|
|
7
8
|
import type { StoredVisibility } from '../content/visibility.ts';
|
|
8
9
|
import type { ResolvedConfig } from '../config.ts';
|
|
@@ -10,9 +11,11 @@ import type { DocumentChange } from '../content/sync.ts';
|
|
|
10
11
|
import type { GeekityEnv } from '../env.ts';
|
|
11
12
|
import type { UndescribedImage } from '../images/alt-text.ts';
|
|
12
13
|
import type { EnclosureForm } from './enclosure-field.ts';
|
|
14
|
+
import type { Refusal } from './editor-layout.ts';
|
|
13
15
|
import type { LocationForm } from './location-field.ts';
|
|
14
16
|
import type { PhotoRow } from './photo-field.ts';
|
|
15
17
|
import type { ReadOfForm } from './read-field.ts';
|
|
18
|
+
import type { ReplyContext } from '../webmention/reply-context.ts';
|
|
16
19
|
/**
|
|
17
20
|
* Everything that differs between the posts screens and the pages screens.
|
|
18
21
|
*
|
|
@@ -124,6 +127,8 @@ export interface DocumentSite {
|
|
|
124
127
|
readonly announce: (change: DocumentChange) => Promise<void>;
|
|
125
128
|
/** The username of whoever is writing: the author a new document starts on. */
|
|
126
129
|
readonly writer: string | undefined;
|
|
130
|
+
readonly citedContext: (target: string) => Promise<ReplyContext | undefined>;
|
|
131
|
+
readonly storedContext: (target: string) => ReplyContext | undefined;
|
|
127
132
|
}
|
|
128
133
|
/** What {@link writeDocument} is asked to write. */
|
|
129
134
|
export interface DocumentWrite {
|
|
@@ -133,6 +138,7 @@ export interface DocumentWrite {
|
|
|
133
138
|
readonly form: EditorForm;
|
|
134
139
|
/** Whether it is saved as a draft, which the editor's buttons decide. */
|
|
135
140
|
readonly draft: boolean;
|
|
141
|
+
readonly keptProperties?: KeptProperties | undefined;
|
|
136
142
|
}
|
|
137
143
|
/** What came of a {@link writeDocument}. */
|
|
138
144
|
export type WriteOutcome = {
|
|
@@ -140,10 +146,9 @@ export type WriteOutcome = {
|
|
|
140
146
|
readonly saved: Document;
|
|
141
147
|
/** Images published without alt text, which the editor warns about. */
|
|
142
148
|
readonly undescribed: readonly UndescribedImage[];
|
|
143
|
-
} | {
|
|
149
|
+
} | ({
|
|
144
150
|
readonly outcome: 'refused';
|
|
145
|
-
|
|
146
|
-
} | {
|
|
151
|
+
} & Refusal) | {
|
|
147
152
|
readonly outcome: 'conflict';
|
|
148
153
|
/** The document as the form loaded it, which only an edit has. */
|
|
149
154
|
readonly document: Document;
|
|
@@ -283,6 +288,16 @@ export interface EditorForm {
|
|
|
283
288
|
contact: boolean;
|
|
284
289
|
/** Whether the post is pinned to its author's profile (TASK-207). Posts only. */
|
|
285
290
|
pinned: boolean;
|
|
291
|
+
/**
|
|
292
|
+
* Whether the post hides the previews of the pages it cites, `preview:
|
|
293
|
+
* false` (TASK-252). Posts only.
|
|
294
|
+
*/
|
|
295
|
+
previewHidden: boolean;
|
|
296
|
+
/**
|
|
297
|
+
* The alt text of an image the post cites, `cited-alt` (TASK-255). Posts
|
|
298
|
+
* only; empty for none, when the post's title describes it.
|
|
299
|
+
*/
|
|
300
|
+
citedAlt: string;
|
|
286
301
|
/**
|
|
287
302
|
* What the document says about comments: one of {@link COMMENT_SETTINGS}.
|
|
288
303
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../src/admin/documents.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../src/admin/documents.ts"],"names":[],"mappings":"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"}
|
package/dist/admin/documents.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, rm } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { CITATION_PROPERTIES, citationText } from '../content/citation.js';
|
|
3
|
+
import { CITATION_PROPERTIES, citationText, CITED_ALT_FRONT_MATTER_KEY, citedHost, PREVIEW_FRONT_MATTER_KEY, previewShown, } from '../content/citation.js';
|
|
4
4
|
import { renderMarkdown } from '../content/markdown.js';
|
|
5
5
|
import { parseDocument } from '../content/parser.js';
|
|
6
6
|
import { PINNED_FRONT_MATTER_KEY, PINNED_POST_LIMIT, pinnedAt } from '../content/pinned.js';
|
|
7
|
-
import { postLabel, replyTarget } from '../content/post-type.js';
|
|
7
|
+
import { discoverPostType, postLabel, replyTarget } from '../content/post-type.js';
|
|
8
8
|
import { isReadStatus, READ_OF_FRONT_MATTER_KEY, READ_STATUS_FRONT_MATTER_KEY, READ_STATUS_LABELS, READ_STATUSES, readWork, readWorkFrontMatter, } from '../content/read.js';
|
|
9
9
|
import { ENCLOSURE_FRONT_MATTER_KEY, enclosureOf, isWebUrl, TRANSCRIPT_TYPES, } from '../content/enclosure.js';
|
|
10
10
|
import { PHOTO_FRONT_MATTER_KEY, photoFrontMatter } from '../content/photo.js';
|
|
11
|
+
import { keptProperties } from '../content/kept-properties.js';
|
|
11
12
|
import { postLocations } from '../content/locations.js';
|
|
12
13
|
import { contentFilePath, freeSlug, saveDocument } from '../content/save.js';
|
|
13
14
|
import { scheduledFor } from '../content/schedule.js';
|
|
@@ -33,10 +34,13 @@ import { ADMIN_PREFIX } from './session.js';
|
|
|
33
34
|
import { ADMIN_TEMPLATES } from './templates.js';
|
|
34
35
|
import { UPLOADS_PATH } from './uploads.js';
|
|
35
36
|
import { BLANK_ALTERNATE_ROW, BLANK_ENCLOSURE_FORM, ENCLOSURE_FIELDS, enclosureChoices, enclosureForm, enclosureFrontMatter, readEnclosureForm, resolveEnclosure, } from './enclosure-field.js';
|
|
37
|
+
import { openGroups } from './editor-layout.js';
|
|
38
|
+
import { allowGeolocation } from './headers.js';
|
|
36
39
|
import { BLANK_LOCATION_FORM, LOCATION_FIELDS, locationForm, readLocationForm, resolveLocation, } from './location-field.js';
|
|
37
40
|
import { PHOTO_FIELDS, photoChoices, photoRows, photoRowViews, readPhotoForm, resolvePhotos, } from './photo-field.js';
|
|
38
41
|
import { BLANK_READ_OF_FORM, READ_FIELDS, readOfForm, resolveRead, submittedReadOfForm, } from './read-field.js';
|
|
39
42
|
import { SYNDICATE_TO_FRONT_MATTER_KEY, syndicateToOf, syndicationTargetsReader, } from '../webmention/syndication.js';
|
|
43
|
+
import { citesAnImage, shownInFull } from '../webmention/cited-picture.js';
|
|
40
44
|
/**
|
|
41
45
|
* The Eleventy key that keeps a document out of every collection.
|
|
42
46
|
*
|
|
@@ -232,6 +236,8 @@ async function saveFromForm(c, options) {
|
|
|
232
236
|
exclude: body['exclude'] !== undefined,
|
|
233
237
|
contact: body['contact'] !== undefined,
|
|
234
238
|
pinned: kind.type === 'post' && body['pinned'] !== undefined,
|
|
239
|
+
previewHidden: kind.type === 'post' && body[PREVIEW_FRONT_MATTER_KEY] !== undefined,
|
|
240
|
+
citedAlt: kind.type === 'post' ? text(body[CITED_ALT_FRONT_MATTER_KEY]).trim() : '',
|
|
235
241
|
comments: commentSetting(text(body['comments'])),
|
|
236
242
|
enclosure: kind.type === 'post' ? readEnclosureForm(body) : BLANK_ENCLOSURE_FORM,
|
|
237
243
|
photos: kind.type === 'post' ? readPhotoForm(body) : [],
|
|
@@ -248,13 +254,13 @@ async function saveFromForm(c, options) {
|
|
|
248
254
|
// The buttons doc-5 names are shortcuts past the checkbox: Save draft and
|
|
249
255
|
// Publish say what they do, and Update leaves the decision to the checkbox.
|
|
250
256
|
const draft = action === 'save-draft' ? true : action === 'publish' ? false : form.draft;
|
|
251
|
-
function refuse(
|
|
257
|
+
function refuse(refusal) {
|
|
252
258
|
return renderEditor(c, {
|
|
253
259
|
kind,
|
|
254
260
|
render,
|
|
255
261
|
document,
|
|
256
262
|
form: { ...form, draft },
|
|
257
|
-
|
|
263
|
+
refusal,
|
|
258
264
|
status: 400,
|
|
259
265
|
});
|
|
260
266
|
}
|
|
@@ -263,9 +269,11 @@ async function saveFromForm(c, options) {
|
|
|
263
269
|
config: c.var.config,
|
|
264
270
|
announce: c.var.announce,
|
|
265
271
|
writer: currentUsername(c),
|
|
272
|
+
citedContext: (target) => c.var.replyContexts.describe(target),
|
|
273
|
+
storedContext: (target) => c.var.replyContexts.read(target),
|
|
266
274
|
}, { kind, document, form, draft });
|
|
267
275
|
if (written.outcome === 'refused')
|
|
268
|
-
return refuse(written
|
|
276
|
+
return refuse(written);
|
|
269
277
|
if (written.outcome === 'conflict') {
|
|
270
278
|
return renderConflict(c, {
|
|
271
279
|
kind,
|
|
@@ -295,52 +303,52 @@ export async function writeDocument(site, write) {
|
|
|
295
303
|
const { store, config, writer } = site;
|
|
296
304
|
const { kind, document, form, draft } = write;
|
|
297
305
|
const contentDir = config.contentDir;
|
|
298
|
-
function refused(message) {
|
|
299
|
-
return { outcome: 'refused', message };
|
|
306
|
+
function refused(message, field) {
|
|
307
|
+
return { outcome: 'refused', message, field };
|
|
300
308
|
}
|
|
301
309
|
// A post with no title is a note; a page is always named.
|
|
302
310
|
if (form.title === '' && kind.type === 'page') {
|
|
303
|
-
return refused(`A ${kind.singular} needs a title
|
|
311
|
+
return refused(`A ${kind.singular} needs a title.`, 'editor-title');
|
|
304
312
|
}
|
|
305
313
|
// A post is a reply only when the target is a URL (Post Type Discovery), so
|
|
306
314
|
// anything else would be saved as a reply that is not one.
|
|
307
315
|
if (kind.type === 'post' && form.inReplyTo !== '' && replyTarget(form) === undefined) {
|
|
308
|
-
return refused('In reply to has to be a web address, like https://example.com/a-post/.');
|
|
316
|
+
return refused('In reply to has to be a web address, like https://example.com/a-post/.', 'editor-in-reply-to');
|
|
309
317
|
}
|
|
310
318
|
let read;
|
|
311
319
|
if (kind.type === 'post') {
|
|
312
320
|
for (const property of CITATION_PROPERTIES) {
|
|
313
321
|
const cited = form[CITATION_FIELDS[property]];
|
|
314
322
|
if (cited !== '' && !isWebUrl(cited)) {
|
|
315
|
-
return refused(`${CITATION_LABELS[property]} has to be a web address, like https://example.com/a-post
|
|
323
|
+
return refused(`${CITATION_LABELS[property]} has to be a web address, like https://example.com/a-post/.`, `editor-${property}`);
|
|
316
324
|
}
|
|
317
325
|
}
|
|
318
326
|
const resolved = resolveRead(form.readStatus, form.readOf);
|
|
319
327
|
if ('error' in resolved)
|
|
320
|
-
return refused(resolved.error);
|
|
328
|
+
return refused(resolved.error, resolved.field);
|
|
321
329
|
read = resolved.read;
|
|
322
330
|
}
|
|
323
331
|
if (read !== undefined && form.description !== '') {
|
|
324
|
-
return refused('A read is described by what it says, so it keeps no Description. Empty Description to save it.');
|
|
332
|
+
return refused('A read is described by what it says, so it keeps no Description. Empty Description to save it.', 'editor-description');
|
|
325
333
|
}
|
|
326
334
|
if (form.lang !== '' && !LANGUAGE_TAG_PATTERN.test(form.lang)) {
|
|
327
|
-
return refused('That is not a language tag, such as en, fr or pt-BR.');
|
|
335
|
+
return refused('That is not a language tag, such as en, fr or pt-BR.', 'editor-lang');
|
|
328
336
|
}
|
|
329
337
|
let media = { recording: undefined, photos: [] };
|
|
330
338
|
if (kind.type === 'post') {
|
|
331
339
|
const resolved = resolveEnclosure(form.enclosure, document === undefined ? undefined : enclosureOf(document.extra), contentDir);
|
|
332
340
|
if ('error' in resolved)
|
|
333
|
-
return refused(resolved.error);
|
|
341
|
+
return refused(resolved.error, resolved.field);
|
|
334
342
|
const photos = resolvePhotos(form.photos, contentDir);
|
|
335
343
|
if ('error' in photos)
|
|
336
|
-
return refused(photos.error);
|
|
344
|
+
return refused(photos.error, photos.field);
|
|
337
345
|
media = { recording: resolved.enclosure, photos: photos.photos };
|
|
338
346
|
}
|
|
339
347
|
let location;
|
|
340
348
|
if (kind.type === 'post') {
|
|
341
349
|
const resolved = resolveLocation(form.location);
|
|
342
350
|
if ('error' in resolved)
|
|
343
|
-
return refused(resolved.error);
|
|
351
|
+
return refused(resolved.error, resolved.field);
|
|
344
352
|
location = resolved.location;
|
|
345
353
|
}
|
|
346
354
|
// TASK-207: a pin is new when the file does not carry one yet, and only a
|
|
@@ -350,7 +358,7 @@ export async function writeDocument(site, write) {
|
|
|
350
358
|
const author = userForAuthor(users, chosenAuthor(config.dataDir, form.author, document, writer) ?? '');
|
|
351
359
|
const names = author === undefined ? [] : authorNames(users, author);
|
|
352
360
|
if (store.listPinnedByAuthor(names).length >= PINNED_POST_LIMIT) {
|
|
353
|
-
return refused(`You can pin up to ${String(PINNED_POST_LIMIT)} posts. Unpin one first
|
|
361
|
+
return refused(`You can pin up to ${String(PINNED_POST_LIMIT)} posts. Unpin one first.`, 'editor-pinned');
|
|
354
362
|
}
|
|
355
363
|
}
|
|
356
364
|
// TASK-141: an image nobody described is a problem to fix before readers
|
|
@@ -366,22 +374,29 @@ export async function writeDocument(site, write) {
|
|
|
366
374
|
if (undescribed.length > 0 && config.requireAltText) {
|
|
367
375
|
return refused(`This site publishes no image without alt text. ${missingAltText(undescribed)}`);
|
|
368
376
|
}
|
|
377
|
+
if (!draft &&
|
|
378
|
+
config.requireAltText &&
|
|
379
|
+
kind.type === 'post' &&
|
|
380
|
+
(await citesUndescribedImage(site, document, form))) {
|
|
381
|
+
return refused('This site publishes no image without alt text. Describe the image this post reposts in its alt text field.', 'editor-cited-alt');
|
|
382
|
+
}
|
|
369
383
|
const timezone = readSiteSettings(contentDir).timezone;
|
|
370
384
|
// decision-11: what the file gets is a UTC instant, and an offset-less field
|
|
371
385
|
// is the site's own wall clock rather than the server's.
|
|
372
386
|
const typed = kind.dated ? (form.date === '' ? store.now().toISOString() : form.date) : undefined;
|
|
373
387
|
if (typed !== undefined && !/^\d{4}-\d{2}-\d{2}/.test(typed)) {
|
|
374
|
-
return refused('A date has to start with a year, a month and a day, like 2026-03-04.');
|
|
388
|
+
return refused('A date has to start with a year, a month and a day, like 2026-03-04.', 'editor-date');
|
|
375
389
|
}
|
|
376
390
|
const date = typed === undefined ? undefined : toUtcInstant(typed, timezone);
|
|
377
391
|
if (typed !== undefined && date === undefined) {
|
|
378
|
-
return refused('That date is not one anybody can read. Try 2026-03-04 09:00.');
|
|
392
|
+
return refused('That date is not one anybody can read. Try 2026-03-04 09:00.', 'editor-date');
|
|
379
393
|
}
|
|
380
394
|
const slug = slugify(form.slug) ||
|
|
381
395
|
slugify(form.title) ||
|
|
382
396
|
(document?.slug ?? '') ||
|
|
383
397
|
noteSlug(form.body) ||
|
|
384
398
|
slugify(form.readOf.name) ||
|
|
399
|
+
(await typeSlug(kind, form, media.photos, site.citedContext)) ||
|
|
385
400
|
'untitled';
|
|
386
401
|
const trashed = document !== undefined && isTrashedPath(document.path);
|
|
387
402
|
// The calendar day the document is filed under: the site zone's day at its
|
|
@@ -419,7 +434,7 @@ export async function writeDocument(site, write) {
|
|
|
419
434
|
}
|
|
420
435
|
}
|
|
421
436
|
if (target !== document?.path && store.getByPath(target) !== undefined) {
|
|
422
|
-
return refused(`Another ${kind.singular} already lives in ${target}
|
|
437
|
+
return refused(`Another ${kind.singular} already lives in ${target}.`, 'editor-slug');
|
|
423
438
|
}
|
|
424
439
|
const content = {
|
|
425
440
|
title: form.title,
|
|
@@ -459,10 +474,15 @@ export async function writeDocument(site, write) {
|
|
|
459
474
|
}
|
|
460
475
|
if (kind.type === 'post') {
|
|
461
476
|
const locations = postLocations(config.dataDir);
|
|
477
|
+
const kept = keptProperties(config.dataDir);
|
|
462
478
|
if (document !== undefined && document.permalink !== saved.permalink) {
|
|
463
479
|
await locations.move(document.permalink, saved.permalink);
|
|
480
|
+
await kept.move(document.permalink, saved.permalink);
|
|
464
481
|
}
|
|
465
482
|
await locations.set(saved.permalink, location);
|
|
483
|
+
if (write.keptProperties !== undefined) {
|
|
484
|
+
await kept.set(saved.permalink, write.keptProperties);
|
|
485
|
+
}
|
|
466
486
|
}
|
|
467
487
|
// Announced rather than left to the watcher: the index already holds what
|
|
468
488
|
// was written, so the watcher's re-read is a no-op and nobody would ever
|
|
@@ -479,6 +499,19 @@ export async function writeDocument(site, write) {
|
|
|
479
499
|
});
|
|
480
500
|
return { outcome: 'saved', saved, undescribed };
|
|
481
501
|
}
|
|
502
|
+
async function citesUndescribedImage(site, document, form) {
|
|
503
|
+
if (form.previewHidden || form.citedAlt !== '' || form.title !== '')
|
|
504
|
+
return false;
|
|
505
|
+
for (const property of CITATION_PROPERTIES) {
|
|
506
|
+
const url = form[CITATION_FIELDS[property]];
|
|
507
|
+
if (url === '' || !shownInFull(property, { kind: 'photo' }))
|
|
508
|
+
continue;
|
|
509
|
+
const context = document === undefined ? await site.citedContext(url) : site.storedContext(url);
|
|
510
|
+
if (context !== undefined && citesAnImage(context))
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
482
515
|
/** What a save says about the images it found with no alt text, naming each. */
|
|
483
516
|
function missingAltText(images) {
|
|
484
517
|
const names = images.map((image) => image.name).join(', ');
|
|
@@ -493,6 +526,51 @@ function noteSlug(body) {
|
|
|
493
526
|
const words = htmlToText(renderMarkdown(body)).split(' ').slice(0, NOTE_SLUG_WORDS);
|
|
494
527
|
return slugify(words.join(' '));
|
|
495
528
|
}
|
|
529
|
+
const TARGET_SLUG_WORDS = 4;
|
|
530
|
+
const CITED_SLUGS = {
|
|
531
|
+
repost: { prefix: 'reposted', field: 'repostOf' },
|
|
532
|
+
like: { prefix: 'liked', field: 'likeOf' },
|
|
533
|
+
reply: { prefix: 'reply-to', field: 'inReplyTo' },
|
|
534
|
+
bookmark: { prefix: 'bookmarked', field: 'bookmarkOf' },
|
|
535
|
+
};
|
|
536
|
+
async function typeSlug(kind, form, photos, citedContext) {
|
|
537
|
+
if (kind.type !== 'post')
|
|
538
|
+
return '';
|
|
539
|
+
const type = discoverPostType({
|
|
540
|
+
'repost-of': form.repostOf,
|
|
541
|
+
'like-of': form.likeOf,
|
|
542
|
+
'in-reply-to': form.inReplyTo,
|
|
543
|
+
'bookmark-of': form.bookmarkOf,
|
|
544
|
+
photo: photos.map((photo) => photo.url),
|
|
545
|
+
});
|
|
546
|
+
if (type === 'photo')
|
|
547
|
+
return 'photo';
|
|
548
|
+
const cited = CITED_SLUGS[type];
|
|
549
|
+
if (cited === undefined)
|
|
550
|
+
return '';
|
|
551
|
+
const target = form[cited.field];
|
|
552
|
+
const title = slugWords((await citedContext(target))?.name ?? '', NOTE_SLUG_WORDS);
|
|
553
|
+
return `${cited.prefix}-${title || targetWords(target)}`;
|
|
554
|
+
}
|
|
555
|
+
function targetWords(address) {
|
|
556
|
+
const url = new URL(address);
|
|
557
|
+
const segments = url.pathname.split('/').map(decodedSegment).filter(holdsALetter);
|
|
558
|
+
return slugWords([url.hostname.replace(/^www\./, ''), ...segments].join(' '), TARGET_SLUG_WORDS);
|
|
559
|
+
}
|
|
560
|
+
function slugWords(text, cap) {
|
|
561
|
+
return slugify(text).split('-').slice(0, cap).join('-');
|
|
562
|
+
}
|
|
563
|
+
function holdsALetter(segment) {
|
|
564
|
+
return /\p{L}/u.test(segment);
|
|
565
|
+
}
|
|
566
|
+
function decodedSegment(segment) {
|
|
567
|
+
try {
|
|
568
|
+
return decodeURIComponent(segment);
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
return segment;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
496
574
|
/** What the flash says after a save, which depends on what the save did. */
|
|
497
575
|
function savedMessage(kind, previous, saved, now) {
|
|
498
576
|
if (saved.draft)
|
|
@@ -676,6 +754,14 @@ function resolveExtra(kind, document, form, now, media, declared) {
|
|
|
676
754
|
else
|
|
677
755
|
extra[property] = cited;
|
|
678
756
|
}
|
|
757
|
+
if (form.previewHidden)
|
|
758
|
+
extra[PREVIEW_FRONT_MATTER_KEY] = false;
|
|
759
|
+
else
|
|
760
|
+
delete extra[PREVIEW_FRONT_MATTER_KEY];
|
|
761
|
+
if (form.citedAlt === '')
|
|
762
|
+
delete extra[CITED_ALT_FRONT_MATTER_KEY];
|
|
763
|
+
else
|
|
764
|
+
extra[CITED_ALT_FRONT_MATTER_KEY] = form.citedAlt;
|
|
679
765
|
const resolved = resolveRead(form.readStatus, form.readOf);
|
|
680
766
|
if ('read' in resolved) {
|
|
681
767
|
const { read } = resolved;
|
|
@@ -1053,6 +1139,8 @@ export function blankForm(kind, timezone = DEFAULT_TIMEZONE, now = new Date()) {
|
|
|
1053
1139
|
exclude: false,
|
|
1054
1140
|
contact: false,
|
|
1055
1141
|
pinned: false,
|
|
1142
|
+
previewHidden: false,
|
|
1143
|
+
citedAlt: '',
|
|
1056
1144
|
comments: COMMENT_SETTINGS.site,
|
|
1057
1145
|
enclosure: BLANK_ENCLOSURE_FORM,
|
|
1058
1146
|
photos: [],
|
|
@@ -1099,6 +1187,10 @@ export function formFor(document, timezone, location) {
|
|
|
1099
1187
|
exclude: document.extra[EXCLUDE_KEY] === true,
|
|
1100
1188
|
contact: document.extra[CONTACT_FRONT_MATTER_KEY] === true,
|
|
1101
1189
|
pinned: pinnedAt(document) !== undefined,
|
|
1190
|
+
previewHidden: document.type === 'post' && !previewShown(document.extra),
|
|
1191
|
+
citedAlt: document.type === 'post' && typeof document.extra[CITED_ALT_FRONT_MATTER_KEY] === 'string'
|
|
1192
|
+
? document.extra[CITED_ALT_FRONT_MATTER_KEY]
|
|
1193
|
+
: '',
|
|
1102
1194
|
comments: commentSettingOf(document),
|
|
1103
1195
|
enclosure: document.type === 'post' ? enclosureForm(document) : BLANK_ENCLOSURE_FORM,
|
|
1104
1196
|
photos: document.type === 'post' ? photoRows(document) : [],
|
|
@@ -1138,6 +1230,10 @@ async function renderEditor(c, options) {
|
|
|
1138
1230
|
actions.push({ value: 'trash', label: 'Move to trash' });
|
|
1139
1231
|
if (options.status !== undefined)
|
|
1140
1232
|
c.status(options.status);
|
|
1233
|
+
const permissions = c.var.config.securityHeaders['permissions-policy'];
|
|
1234
|
+
if (kind.type === 'post' && permissions !== undefined) {
|
|
1235
|
+
c.header('Permissions-Policy', allowGeolocation(permissions));
|
|
1236
|
+
}
|
|
1141
1237
|
return options.render(c, ADMIN_TEMPLATES.documentEditor, {
|
|
1142
1238
|
section: kind.section,
|
|
1143
1239
|
// The blank form is Add new; editing one that exists is still All posts,
|
|
@@ -1158,6 +1254,7 @@ async function renderEditor(c, options) {
|
|
|
1158
1254
|
photoRows: photoRowViews(form.photos, readAltTexts(c.var.config.contentDir)),
|
|
1159
1255
|
photoChoices: await photoChoices(c.var.config.contentDir),
|
|
1160
1256
|
locationFields: LOCATION_FIELDS,
|
|
1257
|
+
...citedPreviews(c, form),
|
|
1161
1258
|
readFields: READ_FIELDS,
|
|
1162
1259
|
readStatuses: READ_STATUSES.map((value) => ({ value, label: READ_STATUS_LABELS[value] })),
|
|
1163
1260
|
...(form.readStatus === '' || isReadStatus(form.readStatus)
|
|
@@ -1187,9 +1284,41 @@ async function renderEditor(c, options) {
|
|
|
1187
1284
|
// exactly the ambiguity decision-11 exists to remove.
|
|
1188
1285
|
...(kind.dated ? { dateZone: zoneLabel(form.date === '' ? now : form.date, timezone) } : {}),
|
|
1189
1286
|
...(scheduledAt === undefined ? {} : { scheduledFor: formatInTimezone(scheduledAt, timezone) }),
|
|
1190
|
-
...(options.
|
|
1287
|
+
...(options.refusal === undefined
|
|
1288
|
+
? {}
|
|
1289
|
+
: { error: options.refusal.message, errorField: options.refusal.field }),
|
|
1290
|
+
open: openGroups(form, options.refusal?.field),
|
|
1191
1291
|
});
|
|
1192
1292
|
}
|
|
1293
|
+
function citedPreviews(c, form) {
|
|
1294
|
+
const cited = [
|
|
1295
|
+
['in-reply-to', form.inReplyTo],
|
|
1296
|
+
...CITATION_PROPERTIES.map((property) => [
|
|
1297
|
+
property,
|
|
1298
|
+
form[CITATION_FIELDS[property]],
|
|
1299
|
+
]),
|
|
1300
|
+
];
|
|
1301
|
+
const cards = {};
|
|
1302
|
+
for (const [property, url] of cited) {
|
|
1303
|
+
if (url === '')
|
|
1304
|
+
continue;
|
|
1305
|
+
const context = c.var.replyContexts.read(url);
|
|
1306
|
+
if (context?.picture === undefined)
|
|
1307
|
+
continue;
|
|
1308
|
+
const image = citesAnImage(context);
|
|
1309
|
+
cards[property] = {
|
|
1310
|
+
name: context.name ??
|
|
1311
|
+
context.author?.name ??
|
|
1312
|
+
`${image ? 'An image from' : 'A page on'} ${citedHost(url)}`,
|
|
1313
|
+
picture: context.picture,
|
|
1314
|
+
described: image && shownInFull(property, context.picture),
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
return {
|
|
1318
|
+
citedPreviews: cards,
|
|
1319
|
+
describesCitedImage: Object.values(cards).some((card) => card.described),
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1193
1322
|
/**
|
|
1194
1323
|
* The document of this kind with this slug.
|
|
1195
1324
|
*
|