11import { createRemoteFileNode } from "gatsby-source-filesystem" ;
2- import { visit , EXIT } from "unist-util-visit" ;
3- import type { Node as UnistNode , Parent as UnistParent } from "unist" ;
2+ import { visit } from "unist-util-visit" ;
3+ import type { Node as UnistNode } from "unist" ;
44import type { Image } from "mdast" ;
5- import { RemarkPluginApi , RemarkStructuredContentTransformer , StructuredContentPluginOptions as RemarkStructuredContentPluginOptions , TransformerContext } from "./types" ;
5+ import { RemarkPluginApi , StructuredContentPluginOptions as RemarkStructuredContentPluginOptions , TransformerContext } from "./utils/types" ;
6+ import { removeNodeFromMdAST } from "utils" ;
67
7- /**
8- * Extract ALL images from the markdown AST and save them to File nodes.
9- */
10- export function createImageExtractorTransformer ( ) : RemarkStructuredContentTransformer < Image > {
11- return {
12- createSchemaCustomization : ( { reporter, actions, schema } ) => {
13- const { createTypes } = actions ;
14-
15- reporter . info ( "Creating schema customization for createImageExtractorTransformer" ) ;
16-
17- const typeDefs = [
18- `
19- type MarkdownRemark implements Node {
20- embeddedImages: [File] @link(by: "fields.imageExtractedFromMarkdownRemarkId", from: "id")
21- }
22- ` ,
23- ]
24-
25- createTypes ( typeDefs ) ;
26- } ,
27- traverse : ( markdownAST , _utils , context ) => {
28- getAllImagesFromMarkdownAST ( markdownAST ) . forEach ( ( imageNode ) => {
29- context . collect ( imageNode ) ;
30- } ) ;
31- } ,
32- transform : async ( context , { createFileNode } , { markdownNode : markdownRemarkGatsbyNode } ) => {
33- for ( const node of context . collected ) {
34- await createFileNode ( node , { imageExtractedFromMarkdownRemarkId : markdownRemarkGatsbyNode . id } ) ;
35- }
36- } ,
37- } ;
38- }
39-
40- export type CreateThumbnailImageTransformerOptions = {
41- keepImageInMdAST ?: boolean ;
42- } ;
43-
44- /**
45- * Extract a single "thumbnail" image with special rules, then remove it from the AST.
46- */
47- export function createThumbnailImageTransformer ( options ?: CreateThumbnailImageTransformerOptions ) : RemarkStructuredContentTransformer < Image > {
48- const { keepImageInMdAST } = options || { } ;
49-
50- const LINK_FIELD_NAME = "thumbnailImage" ;
51-
52- return {
53- createSchemaCustomization : ( { actions, schema } ) => {
54- const { createTypes } = actions ;
55- const typeDefs = `
56- type MarkdownRemark implements Node {
57- ${ LINK_FIELD_NAME } : File @link(from: "fields.${ LINK_FIELD_NAME } ", by: "id")
58- }
59- ` ;
60- createTypes ( typeDefs ) ;
61- } ,
62- traverse : ( markdownAST , _utils , context ) => {
63- const thumbImgNode = getThumbnailImageOnly ( markdownAST ) ;
64-
65- if ( thumbImgNode ) {
66- context . collect ( thumbImgNode ) ;
67- }
68- } ,
69- transform : async ( context , { createFileNode, removeNodeFromMdAST } , gatsbyApis ) => {
70- const { markdownNode : markdownRemarkGatsbyNode , actions } = gatsbyApis ;
71-
72- const [ thumbMdASTNode ] = context . collected ;
73-
74- if ( ! thumbMdASTNode ) {
75- // No thumbnail image found
76- return ;
77- }
78-
79- const { createNodeField } = actions ;
80-
81- const thumbImgGatsbyNode = await createFileNode ( thumbMdASTNode ) ;
82-
83- createNodeField ( { node : markdownRemarkGatsbyNode , name : LINK_FIELD_NAME , value : thumbImgGatsbyNode . id } ) ;
84-
85- if ( keepImageInMdAST === true ) {
86- // do nothing, keep the node in the AST
87- } else {
88- await removeNodeFromMdAST ( thumbMdASTNode ) ;
89- }
90- } ,
91- } ;
92- }
938
949/**
9510 * Main remark plugin entrypoint.
@@ -113,21 +28,22 @@ export default async function remarkStructuredContentPlugin(
11328 const { createNode, createNodeField } = actions ;
11429 const { transformers } = pluginOptions ;
11530
116- async function createFileNode (
117- node : Image ,
118- extraFields : Record < string , unknown > = { }
31+ async function createRemoteFileNodeWithFields (
32+ mdastNode : Image ,
33+ extraFields : Record < string , unknown > = { } ,
34+ parentNodeId ?: string
11935 ) {
120- reporter . info ( `Saving remote file node for image: ${ node . url } ` ) ;
36+ reporter . info ( `Saving remote file node for image: ${ mdastNode . url } ` ) ;
12137
12238 const fileNode = await createRemoteFileNode ( {
123- url : node . url ,
124- parentNodeId : markdownNode . id ,
39+ url : mdastNode . url ,
40+ parentNodeId : parentNodeId ,
12541 getCache,
12642 createNode,
12743 createNodeId,
12844 } ) ;
12945
130- reporter . info ( `Created file node with id: ${ fileNode ?. id } for image: ${ node . url } ` ) ;
46+ reporter . info ( `Created file node with id: ${ fileNode ?. id } for image: ${ mdastNode . url } ` ) ;
13147
13248 for ( const [ key , value ] of Object . entries ( extraFields ) ) {
13349 createNodeField ( { node : fileNode , name : key , value } ) ;
@@ -136,13 +52,6 @@ export default async function remarkStructuredContentPlugin(
13652 return fileNode ;
13753 }
13854
139- async function removeNodeFromMdAST ( node : UnistNode ) : Promise < void > {
140- // Simple strategy: blank out the node but keep its place in the tree.
141- ( node as any ) . type = "html" ;
142- ( node as any ) . children = [ ] ;
143- ( node as any ) . value = "" ;
144- }
145-
14655 for ( const transformer of transformers ) {
14756 const context : TransformerContext < any > = {
14857 collected : [ ] ,
@@ -156,65 +65,17 @@ export default async function remarkStructuredContentPlugin(
15665
15766 await transformer . transform (
15867 context ,
159- { createFileNode , removeNodeFromMdAST } ,
68+ { createRemoteFileNodeWithFields , removeNodeFromMdAST } ,
16069 remarkPluginApi ,
16170 ) ;
16271 }
16372
16473 return markdownAST ;
16574}
16675
167- /**
168- * Helpers
169- */
170-
171- function getAllImagesFromMarkdownAST ( markdownAST : UnistNode ) : Image [ ] {
172- const images : Image [ ] = [ ] ;
173-
174- visit ( markdownAST , "image" , ( node ) => {
175- images . push ( node as Image ) ;
176- } ) ;
177-
178- return images ;
179- }
180-
181- /// Return an image node only if there is no text content before the image
182- /// or if the content after the image is only whitespace
183- function getThumbnailImageOnly ( markdownAST : UnistNode ) : Image | null {
184- let thumbnailImage : Image | null = null ;
185-
186- visit (
187- markdownAST ,
188- "image" ,
189- ( node , index , parent ) => {
190- thumbnailImage = node as Image ;
191- return [ EXIT ] ;
192- if ( ! parent || typeof index !== "number" ) {
193- return ;
194- }
195-
196- const p = parent as UnistParent & { children : UnistNode [ ] } ;
197- const nodesBefore = p . children . slice ( 0 , index ) ;
198- const nodesAfter = p . children . slice ( index + 1 ) ;
199-
200- const hasTextBefore = nodesBefore . some (
201- ( n : any ) => n . type === "text" && typeof n . value === "string" && n . value . trim ( ) . length > 0
202- ) ;
203- const hasTextAfter = nodesAfter . some (
204- ( n : any ) => n . type === "text" && typeof n . value === "string" && n . value . trim ( ) . length > 0
205- ) ;
206-
207- if ( ! hasTextBefore && ! hasTextAfter ) {
208- thumbnailImage = node as Image ;
209- return [ EXIT ] ;
210- }
211- }
212- ) ;
213-
214- return thumbnailImage ;
215- }
21676
217- export { sourceNodes } from "./source-nodes" ;
218- export { onCreateNode } from "./on-create-node" ;
219- export { createSchemaCustomization } from "./create-schema-customization" ;
220- export { pluginOptionsSchema } from "./plugin-options-schema" ;
77+ export { sourceNodes } from "./gatsby-apis/source-nodes" ;
78+ export { onCreateNode } from "./gatsby-apis/on-create-node" ;
79+ export { createSchemaCustomization } from "./gatsby-apis/create-schema-customization" ;
80+ export { pluginOptionsSchema } from "./gatsby-apis/plugin-options-schema" ;
81+ export * from "./transformers/index" ;
0 commit comments