@byline/cli 6.3.0 → 6.5.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/dist/templates/ui-byline/components/inline-image/index.tsx +66 -4
- package/dist/templates/ui-byline/components/link/link-lexical.tsx +1 -1
- package/dist/templates/ui-byline/components/richtext-lexical/serialize/index.tsx +45 -2
- package/dist/templates/ui-byline/components/richtext-lexical/serialize/types.ts +16 -7
- package/package.json +9 -9
|
@@ -7,6 +7,8 @@ import type { InlineImagePosition, SerializedInlineImageNode } from '@byline/ric
|
|
|
7
7
|
import { FadeInLift } from '@byline/ui/react'
|
|
8
8
|
|
|
9
9
|
import { ResponsiveImage } from '../responsive-image/index.tsx'
|
|
10
|
+
import { getHref, type LinkAttributes } from '../link/link-lexical.tsx'
|
|
11
|
+
import { LangLink } from '@/ui/byline/components/link/lang-link'
|
|
10
12
|
import type { Locale } from '@/ui/byline/types/i18n'
|
|
11
13
|
import type { SerializeOptions, SerializeProps } from '../richtext-lexical/serialize/index.tsx'
|
|
12
14
|
|
|
@@ -44,7 +46,7 @@ interface Props {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
export function InlineImageSerializer({ node, serialize, lng, options }: Props): React.JSX.Element {
|
|
47
|
-
const { showCaption, position, altText, document: pickerDoc, caption } = node
|
|
49
|
+
const { showCaption, position, altText, document: pickerDoc, caption, link } = node
|
|
48
50
|
const image = pickerDoc?.image as StoredFileValue | undefined
|
|
49
51
|
|
|
50
52
|
const floatLeft = position === 'left'
|
|
@@ -86,16 +88,76 @@ export function InlineImageSerializer({ node, serialize, lng, options }: Props):
|
|
|
86
88
|
</FadeInLift>
|
|
87
89
|
)
|
|
88
90
|
|
|
91
|
+
// Optional click-through target.
|
|
92
|
+
//
|
|
93
|
+
// The anchor goes INSIDE the floated wrapper and wraps the image only.
|
|
94
|
+
// Two reasons, both load-bearing:
|
|
95
|
+
//
|
|
96
|
+
// - the float and the 50% column live on the outer span, so an anchor
|
|
97
|
+
// outside it would take the float and change the layout;
|
|
98
|
+
// - the caption is a nested editor that can contain its own links, and
|
|
99
|
+
// an `<a>` inside an `<a>` is invalid — browsers un-nest it and the
|
|
100
|
+
// layout breaks with it. The caption stays outside the anchor.
|
|
101
|
+
//
|
|
102
|
+
// `display: block` stops the inline `<a>` adding a descender gap beneath
|
|
103
|
+
// the image. `getHref` returns '' for a target the walker marked
|
|
104
|
+
// unresolved, in which case the image renders with no anchor at all —
|
|
105
|
+
// the same degradation a text link gets.
|
|
106
|
+
//
|
|
107
|
+
// `options.insideLink` suppresses the anchor entirely: the image is
|
|
108
|
+
// already inside a text link, whose anchor would be invalidated by a
|
|
109
|
+
// nested one. The outer link wins.
|
|
110
|
+
const href = options?.insideLink === true || link == null ? '' : getHref(link as LinkAttributes)
|
|
111
|
+
|
|
112
|
+
// Target and rel are derived here rather than through the text link's
|
|
113
|
+
// `getAdditionalProps`. That helper forces `target="_blank"` for any
|
|
114
|
+
// external href regardless of the node's own `newTab`, which suits a text
|
|
115
|
+
// link (they also get a visible external-link icon) but makes the image
|
|
116
|
+
// dialog's "Open in new tab" checkbox a no-op for exactly the URLs an
|
|
117
|
+
// editor is most likely to tick it for. Here the editor's choice is
|
|
118
|
+
// authoritative.
|
|
119
|
+
//
|
|
120
|
+
// `rel="noopener"` is belt-and-braces, not a fix for a live hole: the
|
|
121
|
+
// HTML standard has `target="_blank"` imply `noopener` unless a rel
|
|
122
|
+
// explicitly opts back in, and conforming browsers have done so for
|
|
123
|
+
// years. It is stated anyway so the guarantee does not rest on the
|
|
124
|
+
// implied behaviour, and it matches what text links emit for `newTab`.
|
|
125
|
+
// `noreferrer` is deliberately NOT added — it would additionally strip
|
|
126
|
+
// the referrer, a separate behavioural choice that belongs with the
|
|
127
|
+
// site's referrer policy rather than with this checkbox.
|
|
128
|
+
const newTab = link?.newTab === true
|
|
129
|
+
const anchorProps = newTab
|
|
130
|
+
? { target: '_blank', rel: 'noopener' }
|
|
131
|
+
: ({} as { target?: string; rel?: string })
|
|
132
|
+
|
|
133
|
+
const Linked =
|
|
134
|
+
href.length === 0 ? (
|
|
135
|
+
ImgSlot
|
|
136
|
+
) : href.startsWith('/') ? (
|
|
137
|
+
<LangLink lng={lng} to={href} {...anchorProps} className="block">
|
|
138
|
+
{ImgSlot}
|
|
139
|
+
</LangLink>
|
|
140
|
+
) : (
|
|
141
|
+
<a href={href} {...anchorProps} className="block">
|
|
142
|
+
{ImgSlot}
|
|
143
|
+
</a>
|
|
144
|
+
)
|
|
145
|
+
|
|
89
146
|
if (showCaption) {
|
|
90
147
|
return (
|
|
91
148
|
<span className={classes}>
|
|
92
|
-
{
|
|
149
|
+
{Linked}
|
|
93
150
|
<span className="block inline-image-block--caption">
|
|
94
151
|
{caption?.editorState?.root?.children != null ? (
|
|
152
|
+
// The caption is a nested serialize pass and can hold its own
|
|
153
|
+
// links, so the surrounding options travel with it. Dropping
|
|
154
|
+
// them would let a caption link emit an anchor inside the text
|
|
155
|
+
// link this image sits in — the image's own anchor is already
|
|
156
|
+
// suppressed for that reason, and the caption is no different.
|
|
95
157
|
serialize({
|
|
96
158
|
nodes: caption.editorState.root.children,
|
|
97
159
|
lng,
|
|
98
|
-
options: { renderParagraphInline: true },
|
|
160
|
+
options: { ...options, renderParagraphInline: true },
|
|
99
161
|
})
|
|
100
162
|
) : (
|
|
101
163
|
<span>Caption not found for inline image.</span>
|
|
@@ -105,5 +167,5 @@ export function InlineImageSerializer({ node, serialize, lng, options }: Props):
|
|
|
105
167
|
)
|
|
106
168
|
}
|
|
107
169
|
|
|
108
|
-
return <span className={classes}>{
|
|
170
|
+
return <span className={classes}>{Linked}</span>
|
|
109
171
|
}
|
|
@@ -108,7 +108,7 @@ function isLocalHref(href: string): boolean {
|
|
|
108
108
|
* walked sessions.
|
|
109
109
|
* 4. Neither — strip wrapper.
|
|
110
110
|
*/
|
|
111
|
-
function getHref(args: LinkAttributes): string {
|
|
111
|
+
export function getHref(args: LinkAttributes): string {
|
|
112
112
|
let href = ''
|
|
113
113
|
const publicWebsiteUrl = '/' // getPublicWebsiteUrl()
|
|
114
114
|
|
|
@@ -10,7 +10,7 @@ import { CodeSerializer } from '../../code/code-serializer.tsx'
|
|
|
10
10
|
import { HeadingWithAnchorSerializer } from '../../heading-anchor/index.ts'
|
|
11
11
|
import { InlineImageSerializer } from '../../inline-image/index.tsx'
|
|
12
12
|
import { LayoutContainerSerializer, LayoutItemSerializer } from '../../layout/index.tsx'
|
|
13
|
-
import { LinkLexicalSerializer } from '../../link/link-lexical.tsx'
|
|
13
|
+
import { getHref, LinkLexicalSerializer } from '../../link/link-lexical.tsx'
|
|
14
14
|
import { ListItemSerializer, ListSerializer } from '../../list/index.ts'
|
|
15
15
|
import { TableCellSerializer } from '../../table-cell/index.tsx'
|
|
16
16
|
import { VimeoSerializer } from '../../vimeo/index.tsx'
|
|
@@ -30,6 +30,20 @@ import type { SerializedLexicalNode } from './types.ts'
|
|
|
30
30
|
export interface SerializeOptions {
|
|
31
31
|
renderParagraphInline: boolean
|
|
32
32
|
disableAnimation?: boolean
|
|
33
|
+
/**
|
|
34
|
+
* True while serializing the children of a `link` node.
|
|
35
|
+
*
|
|
36
|
+
* An `<a>` may not contain another `<a>`: the HTML parser un-nests them,
|
|
37
|
+
* which silently changes the document structure and breaks the layout
|
|
38
|
+
* around it. An inline image carries its own optional click target, and
|
|
39
|
+
* an image inside a text link is reachable — a markdown import turns
|
|
40
|
+
* `[](url)` into a `link` node containing an `inline-image`
|
|
41
|
+
* node, and the image dialog can then give that image a link of its own.
|
|
42
|
+
*
|
|
43
|
+
* The outer link wins: it already makes the image clickable, which is
|
|
44
|
+
* what a reader expects from an image inside a link.
|
|
45
|
+
*/
|
|
46
|
+
insideLink?: boolean
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
export interface SerializeProps {
|
|
@@ -148,9 +162,38 @@ export function Serialize({
|
|
|
148
162
|
)
|
|
149
163
|
}
|
|
150
164
|
case 'link': {
|
|
165
|
+
// `LinkLexicalSerializer` renders its children plain when the
|
|
166
|
+
// href is unusable (an unresolved internal target, an empty
|
|
167
|
+
// custom url), so a link node does not always produce an
|
|
168
|
+
// anchor. `insideLink` must track the anchor, not the node —
|
|
169
|
+
// setting it unconditionally would strip a working image link
|
|
170
|
+
// for the benefit of an outer anchor that was never emitted.
|
|
171
|
+
//
|
|
172
|
+
// When an anchor IS emitted, children are re-serialized with
|
|
173
|
+
// the flag rather than reusing `serializedChildren`, so nested
|
|
174
|
+
// anchor-bearing nodes (an inline image with its own link)
|
|
175
|
+
// render without their own `<a>`.
|
|
176
|
+
//
|
|
177
|
+
// A link node reached while already inside an anchor renders its
|
|
178
|
+
// children plain. `<a>` inside `<a>` is un-nested by the parser
|
|
179
|
+
// whatever produced it, and a link node is just as capable of
|
|
180
|
+
// nesting as an inline image — an image caption, for instance,
|
|
181
|
+
// is a nested editor whose links arrive here with the flag set.
|
|
182
|
+
if (options?.insideLink === true) {
|
|
183
|
+
return <Fragment key={index}>{serializedChildren}</Fragment>
|
|
184
|
+
}
|
|
185
|
+
const rendersAnchor = getHref(node.attributes).length > 0
|
|
151
186
|
return (
|
|
152
187
|
<LinkLexicalSerializer key={index} attributes={node.attributes} lng={lng}>
|
|
153
|
-
{
|
|
188
|
+
{node.children != null && rendersAnchor ? (
|
|
189
|
+
<Serialize
|
|
190
|
+
nodes={node.children}
|
|
191
|
+
lng={lng}
|
|
192
|
+
options={{ ...options, insideLink: true }}
|
|
193
|
+
/>
|
|
194
|
+
) : (
|
|
195
|
+
serializedChildren
|
|
196
|
+
)}
|
|
154
197
|
</LinkLexicalSerializer>
|
|
155
198
|
)
|
|
156
199
|
}
|
|
@@ -6,10 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
10
|
-
SerializedLexicalNode as LexicalSerializedLexicalNode,
|
|
11
|
-
SerializedRootNode as LexicalSerializedRootNode,
|
|
12
|
-
} from '@byline/richtext-lexical'
|
|
9
|
+
import type { SerializedRootNode as LexicalSerializedRootNode } from '@byline/richtext-lexical'
|
|
13
10
|
|
|
14
11
|
/**
|
|
15
12
|
* Root of a Lexical editor state — `editorState.root` from a persisted
|
|
@@ -33,10 +30,22 @@ export interface SerializedLexicalEditorState {
|
|
|
33
30
|
* `checked`, `attributes`, `headerState`, `kind`, etc.) without
|
|
34
31
|
* enumerating every node type.
|
|
35
32
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* Deliberately standalone rather than an intersection with Lexical's
|
|
34
|
+
* `SerializedLexicalNode`. Lexical 0.51 types a nested editor's children —
|
|
35
|
+
* an inline image's caption, say — as `SerializedPartialNode[]`, whose
|
|
36
|
+
* `version` and `$slots` relax because a compact export omits what parsing
|
|
37
|
+
* restores on its own. Intersecting with the strict type reimposes those
|
|
38
|
+
* requirements at every depth (`$slots` recurses), which would reject the
|
|
39
|
+
* partial form over properties this dispatcher never reads and force a cast
|
|
40
|
+
* at every nested-editor call site.
|
|
41
|
+
*
|
|
42
|
+
* The open index signature is what makes both flavours assignable, so
|
|
43
|
+
* strict and partial nodes alike flow through the dispatcher without casts.
|
|
38
44
|
*/
|
|
39
|
-
export type SerializedLexicalNode =
|
|
45
|
+
export type SerializedLexicalNode = {
|
|
46
|
+
type: string
|
|
47
|
+
/** @deprecated Lexical writes it, nothing reads it; a compact export omits it. */
|
|
48
|
+
version?: number
|
|
40
49
|
format?: number | string
|
|
41
50
|
text?: string
|
|
42
51
|
mode?: string
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/cli",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "6.
|
|
5
|
+
"version": "6.5.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"dist"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@clack/prompts": "^1.8.
|
|
36
|
+
"@clack/prompts": "^1.8.1",
|
|
37
37
|
"commander": "^15.0.0",
|
|
38
38
|
"diff": "^9.0.0",
|
|
39
39
|
"drizzle-orm": "^0.45.2",
|
|
@@ -45,23 +45,23 @@
|
|
|
45
45
|
"picocolors": "^1.1.1",
|
|
46
46
|
"semver": "^7.8.5",
|
|
47
47
|
"ts-morph": "^28.0.0",
|
|
48
|
-
"yaml": "^2.9.
|
|
48
|
+
"yaml": "^2.9.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@biomejs/biome": "2.5.
|
|
51
|
+
"@biomejs/biome": "2.5.14",
|
|
52
52
|
"@types/mdast": "^4.0.4",
|
|
53
|
-
"@types/node": "^26.
|
|
53
|
+
"@types/node": "^26.6.2",
|
|
54
54
|
"@types/pg": "^8.23.1",
|
|
55
55
|
"@types/semver": "^7.8.0",
|
|
56
|
-
"dotenv": "^
|
|
56
|
+
"dotenv": "^18.0.1",
|
|
57
57
|
"mdast-util-to-string": "^4.0.0",
|
|
58
58
|
"remark-gfm": "^4.0.1",
|
|
59
59
|
"remark-parse": "^11.0.0",
|
|
60
|
-
"tsc-alias": "^1.9.
|
|
60
|
+
"tsc-alias": "^1.9.5",
|
|
61
61
|
"typescript": "^7.0.2",
|
|
62
62
|
"unified": "^11.0.5",
|
|
63
|
-
"vitest": "^5.0.
|
|
64
|
-
"@byline/core": "6.
|
|
63
|
+
"vitest": "^5.0.1",
|
|
64
|
+
"@byline/core": "6.5.0"
|
|
65
65
|
},
|
|
66
66
|
"publishConfig": {
|
|
67
67
|
"access": "public",
|