From f1c15f470909b289ec558e748fb4002f9a64e9ec Mon Sep 17 00:00:00 2001 From: Chitoku Date: Thu, 24 Jan 2019 03:17:20 +0900 Subject: [PATCH] chore(gatsby-transformer-remark): add excerptAst to be exported as a GraphQL field --- .../__snapshots__/extend-node.js.snap | 221 ++++++++++++++++++ .../src/__tests__/extend-node.js | 202 ++++++++++++++++ .../src/extend-node-type.js | 210 +++++++++++------ 3 files changed, 562 insertions(+), 71 deletions(-) diff --git a/packages/gatsby-transformer-remark/src/__tests__/__snapshots__/extend-node.js.snap b/packages/gatsby-transformer-remark/src/__tests__/__snapshots__/extend-node.js.snap index 06a0ec6fb31de..5055f206ec733 100644 --- a/packages/gatsby-transformer-remark/src/__tests__/__snapshots__/extend-node.js.snap +++ b/packages/gatsby-transformer-remark/src/__tests__/__snapshots__/extend-node.js.snap @@ -3,6 +3,13 @@ exports[`Excerpt is generated correctly from schema correctly loads a default excerpt 1`] = ` Object { "excerpt": "", + "excerptAst": Object { + "children": Array [], + "data": Object { + "quirksMode": false, + }, + "type": "root", + }, "frontmatter": Object { "title": "my little pony", }, @@ -12,6 +19,17 @@ Object { exports[`Excerpt is generated correctly from schema correctly loads an excerpt 1`] = ` Object { "excerpt": "Where oh where is my little pony?", + "excerptAst": Object { + "children": Array [ + Object { + "type": "text", + "value": "Where oh where is my little pony?", + }, + ], + "properties": Object {}, + "tagName": "p", + "type": "element", + }, "frontmatter": Object { "title": "my little pony", }, @@ -21,6 +39,17 @@ Object { exports[`Excerpt is generated correctly from schema correctly prunes length to default value 1`] = ` Object { "excerpt": "Where oh where is my little pony? Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi auctor sit amet velit id facilisis. Nulla…", + "excerptAst": Object { + "children": Array [ + Object { + "type": "text", + "value": "Where oh where is my little pony? Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi auctor sit amet velit id facilisis. Nulla…", + }, + ], + "properties": Object {}, + "tagName": "p", + "type": "element", + }, "frontmatter": Object { "title": "my little pony", }, @@ -30,6 +59,17 @@ Object { exports[`Excerpt is generated correctly from schema correctly prunes length to provided parameter 1`] = ` Object { "excerpt": "Where oh where is my little pony? Lorem ipsum…", + "excerptAst": Object { + "children": Array [ + Object { + "type": "text", + "value": "Where oh where is my little pony? Lorem ipsum…", + }, + ], + "properties": Object {}, + "tagName": "p", + "type": "element", + }, "frontmatter": Object { "title": "my little pony", }, @@ -39,6 +79,17 @@ Object { exports[`Excerpt is generated correctly from schema correctly prunes length to provided parameter with truncate 1`] = ` Object { "excerpt": "Where oh where is my little pony? Lorem ipsum dol…", + "excerptAst": Object { + "children": Array [ + Object { + "type": "text", + "value": "Where oh where is my little pony? Lorem ipsum dol…", + }, + ], + "properties": Object {}, + "tagName": "p", + "type": "element", + }, "frontmatter": Object { "title": "my little pony", }, @@ -48,6 +99,30 @@ Object { exports[`Excerpt is generated correctly from schema correctly uses excerpt separator 1`] = ` Object { "excerpt": "Where oh where is my little pony? Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi auctor sit amet velit id facilisis. Nulla…", + "excerptAst": Object { + "children": Array [ + Object { + "children": Array [ + Object { + "type": "text", + "value": "Where oh where is my little pony?", + }, + ], + "properties": Object {}, + "tagName": "p", + "type": "element", + }, + Object { + "type": "text", + "value": " +", + }, + ], + "data": Object { + "quirksMode": false, + }, + "type": "root", + }, "frontmatter": Object { "title": "my little pony", }, @@ -57,6 +132,76 @@ Object { exports[`Excerpt is generated correctly from schema given an html format, it correctly maps nested markdown to html 1`] = ` Object { "excerpt": "

Where oh where is \\"that?

", + "excerptAst": Object { + "children": Array [ + Object { + "type": "text", + "value": "Where oh ", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "type": "text", + "value": "where", + }, + ], + "properties": Object {}, + "tagName": "em", + "type": "element", + }, + ], + "properties": Object { + "href": "nick.com", + }, + "tagName": "a", + "type": "element", + }, + Object { + "type": "text", + "value": " ", + }, + Object { + "children": Array [ + Object { + "children": Array [ + Object { + "type": "text", + "value": "is", + }, + ], + "properties": Object {}, + "tagName": "em", + "type": "element", + }, + ], + "properties": Object {}, + "tagName": "strong", + "type": "element", + }, + Object { + "type": "text", + "value": " ", + }, + Object { + "children": Array [], + "properties": Object { + "alt": "that pony", + "src": "pony.png", + }, + "tagName": "img", + "type": "element", + }, + Object { + "type": "text", + "value": "?", + }, + ], + "properties": Object {}, + "tagName": "p", + "type": "element", + }, "frontmatter": Object { "title": "my little pony", }, @@ -66,6 +211,17 @@ Object { exports[`Excerpt is generated correctly from schema given an html format, it prunes large excerpts 1`] = ` Object { "excerpt": "

Where oh where is that pony? Is he in the stable…

", + "excerptAst": Object { + "children": Array [ + Object { + "type": "text", + "value": "Where oh where is that pony? Is he in the stable…", + }, + ], + "properties": Object {}, + "tagName": "p", + "type": "element", + }, "frontmatter": Object { "title": "my little pony", }, @@ -76,6 +232,45 @@ exports[`Excerpt is generated correctly from schema given an html format, it res Object { "excerpt": "

Where oh where is that pony? Is he in the stable or by the stream?

", + "excerptAst": Object { + "children": Array [ + Object { + "children": Array [ + Object { + "type": "text", + "value": "Where oh where is that ", + }, + Object { + "children": Array [ + Object { + "type": "text", + "value": "pony", + }, + ], + "properties": Object {}, + "tagName": "em", + "type": "element", + }, + Object { + "type": "text", + "value": "? Is he in the stable or by the stream?", + }, + ], + "properties": Object {}, + "tagName": "p", + "type": "element", + }, + Object { + "type": "text", + "value": " +", + }, + ], + "data": Object { + "quirksMode": false, + }, + "type": "root", + }, "frontmatter": Object { "title": "my little pony", }, @@ -85,6 +280,32 @@ Object { exports[`Excerpt is generated correctly from schema given raw html in the text body, this html is not escaped 1`] = ` Object { "excerpt": "

Where is my pony named leo?

", + "excerptAst": Object { + "children": Array [ + Object { + "type": "text", + "value": "Where is my ", + }, + Object { + "children": Array [ + Object { + "type": "text", + "value": "pony", + }, + ], + "properties": Object {}, + "tagName": "code", + "type": "element", + }, + Object { + "type": "text", + "value": " named leo?", + }, + ], + "properties": Object {}, + "tagName": "p", + "type": "element", + }, "frontmatter": Object { "title": "my little pony", }, diff --git a/packages/gatsby-transformer-remark/src/__tests__/extend-node.js b/packages/gatsby-transformer-remark/src/__tests__/extend-node.js index 47686bcf7d92a..3b6a928d84ff4 100644 --- a/packages/gatsby-transformer-remark/src/__tests__/extend-node.js +++ b/packages/gatsby-transformer-remark/src/__tests__/extend-node.js @@ -138,6 +138,7 @@ date: "2017-09-18T23:19:51.246Z" --- Where oh where is my little pony?`, `excerpt + excerptAst frontmatter { title } @@ -145,6 +146,17 @@ Where oh where is my little pony?`, node => { expect(node).toMatchSnapshot() expect(node.excerpt).toMatch(`Where oh where is my little pony?`) + expect(node.excerptAst).toMatchObject({ + children: [ + { + type: `text`, + value: `Where oh where is my little pony?`, + }, + ], + properties: {}, + tagName: `p`, + type: `element`, + }) } ) @@ -155,6 +167,7 @@ title: "my little pony" date: "2017-09-18T23:19:51.246Z" ---`, `excerpt + excerptAst frontmatter { title } @@ -162,6 +175,13 @@ date: "2017-09-18T23:19:51.246Z" node => { expect(node).toMatchSnapshot() expect(node.excerpt).toMatch(``) + expect(node.excerptAst).toMatchObject({ + children: [], + data: { + quirksMode: false, + }, + type: `root`, + }) } ) @@ -178,6 +198,7 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi auctor sit amet v In quis lectus sed eros efficitur luctus. Morbi tempor, nisl eget feugiat tincidunt, sem velit vulputate enim, nec interdum augue enim nec mauris. Nulla iaculis ante sed enim placerat pretium. Nulla metus odio, facilisis vestibulum lobortis vitae, bibendum at nunc. Donec sit amet efficitur metus, in bibendum nisi. Vivamus tempus vel turpis sit amet auctor. Maecenas luctus vestibulum velit, at sagittis leo volutpat quis. Praesent posuere nec augue eget sodales. Pellentesque vitae arcu ut est varius venenatis id maximus sem. Curabitur non consectetur turpis. `, `excerpt + excerptAst frontmatter { title } @@ -185,6 +206,29 @@ In quis lectus sed eros efficitur luctus. Morbi tempor, nisl eget feugiat tincid node => { expect(node).toMatchSnapshot() expect(node.excerpt).toMatch(`Where oh where is my little pony?`) + expect(node.excerptAst).toMatchObject({ + children: [ + { + children: [ + { + type: `text`, + value: `Where oh where is my little pony?`, + }, + ], + properties: {}, + tagName: `p`, + type: `element`, + }, + { + type: `text`, + value: `\n`, + }, + ], + data: { + quirksMode: false, + }, + type: `root`, + }) }, { pluginOptions: { excerpt_separator: `` } } ) @@ -201,6 +245,7 @@ In quis lectus sed eros efficitur luctus. Morbi tempor, nisl eget feugiat tincid `correctly prunes length to default value`, content, `excerpt + excerptAst frontmatter { title } @@ -208,6 +253,8 @@ In quis lectus sed eros efficitur luctus. Morbi tempor, nisl eget feugiat tincid node => { expect(node).toMatchSnapshot() expect(node.excerpt.length).toBe(139) + expect(node.excerptAst.children.length).toBe(1) + expect(node.excerptAst.children[0].value.length).toBe(139) } ) @@ -215,6 +262,7 @@ In quis lectus sed eros efficitur luctus. Morbi tempor, nisl eget feugiat tincid `correctly prunes length to provided parameter`, content, `excerpt(pruneLength: 50) + excerptAst(pruneLength: 50) frontmatter { title } @@ -222,6 +270,8 @@ In quis lectus sed eros efficitur luctus. Morbi tempor, nisl eget feugiat tincid node => { expect(node).toMatchSnapshot() expect(node.excerpt.length).toBe(46) + expect(node.excerptAst.children.length).toBe(1) + expect(node.excerptAst.children[0].value.length).toBe(46) } ) @@ -229,6 +279,7 @@ In quis lectus sed eros efficitur luctus. Morbi tempor, nisl eget feugiat tincid `correctly prunes length to provided parameter with truncate`, content, `excerpt(pruneLength: 50, truncate: true) + excerptAst(pruneLength: 50, truncate: true) frontmatter { title } @@ -236,6 +287,8 @@ In quis lectus sed eros efficitur luctus. Morbi tempor, nisl eget feugiat tincid node => { expect(node).toMatchSnapshot() expect(node.excerpt.length).toBe(50) + expect(node.excerptAst.children.length).toBe(1) + expect(node.excerptAst.children[0].value.length).toBe(50) } ) @@ -248,6 +301,7 @@ date: "2017-09-18T23:19:51.246Z" Where oh [*where*](nick.com) **_is_** ![that pony](pony.png)?`, `excerpt(format: HTML) + excerptAst frontmatter { title } @@ -257,6 +311,76 @@ Where oh [*where*](nick.com) **_is_** ![that pony](pony.png)?`, expect(node.excerpt).toMatch( `

Where oh where is that pony?

` ) + expect(node.excerptAst).toMatchObject({ + children: [ + { + type: `text`, + value: `Where oh `, + }, + { + children: [ + { + children: [ + { + type: `text`, + value: `where`, + }, + ], + properties: {}, + tagName: `em`, + type: `element`, + }, + ], + properties: { + href: `nick.com`, + }, + tagName: `a`, + type: `element`, + }, + { + type: `text`, + value: ` `, + }, + { + children: [ + { + children: [ + { + type: `text`, + value: `is`, + }, + ], + properties: {}, + tagName: `em`, + type: `element`, + }, + ], + properties: {}, + tagName: `strong`, + type: `element`, + }, + { + type: `text`, + value: ` `, + }, + { + children: [], + properties: { + alt: `that pony`, + src: `pony.png`, + }, + tagName: `img`, + type: `element`, + }, + { + type: `text`, + value: `?`, + }, + ], + properties: {}, + tagName: `p`, + type: `element`, + }) } ) @@ -269,6 +393,7 @@ date: "2017-09-18T23:19:51.246Z" Where is my pony named leo?`, `excerpt(format: HTML) + excerptAst frontmatter { title } @@ -278,6 +403,32 @@ Where is my pony named leo?`, expect(node.excerpt).toMatch( `

Where is my pony named leo?

` ) + expect(node.excerptAst).toMatchObject({ + children: [ + { + type: `text`, + value: `Where is my `, + }, + { + children: [ + { + type: `text`, + value: `pony`, + }, + ], + properties: {}, + tagName: `code`, + type: `element`, + }, + { + type: `text`, + value: ` named leo?`, + }, + ], + properties: {}, + tagName: `p`, + type: `element`, + }) }, { pluginOptions: { excerpt_separator: `` } } ) @@ -291,6 +442,7 @@ date: "2017-09-18T23:19:51.246Z" Where oh where is that pony? Is he in the stable or down by the stream?`, `excerpt(format: HTML, pruneLength: 50) + excerptAst(pruneLength: 50) frontmatter { title } @@ -300,6 +452,17 @@ Where oh where is that pony? Is he in the stable or down by the stream?`, expect(node.excerpt).toMatch( `

Where oh where is that pony? Is he in the stable…

` ) + expect(node.excerptAst).toMatchObject({ + children: [ + { + type: `text`, + value: `Where oh where is that pony? Is he in the stable…`, + }, + ], + properties: {}, + tagName: `p`, + type: `element`, + }) } ) @@ -316,6 +479,7 @@ Where oh where is that *pony*? Is he in the stable or by the stream? Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi auctor sit amet velit id facilisis. Nulla viverra, eros at efficitur pulvinar, lectus orci accumsan nisi, eu blandit elit nulla nec lectus. Integer porttitor imperdiet sapien. Quisque in orci sed nisi consequat aliquam. Aenean id mollis nisi. Sed auctor odio id erat facilisis venenatis. Quisque posuere faucibus libero vel fringilla. `, `excerpt(format: HTML, pruneLength: 50) + excerptAst(pruneLength: 50) frontmatter { title } @@ -325,6 +489,44 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi auctor sit amet v expect(node.excerpt).toMatch( `

Where oh where is that pony? Is he in the stable or by the stream?

` ) + expect(node.excerptAst).toMatchObject({ + children: [ + { + children: [ + { + type: `text`, + value: `Where oh where is that `, + }, + { + children: [ + { + type: `text`, + value: `pony`, + }, + ], + properties: {}, + tagName: `em`, + type: `element`, + }, + { + type: `text`, + value: `? Is he in the stable or by the stream?`, + }, + ], + properties: {}, + tagName: `p`, + type: `element`, + }, + { + type: `text`, + value: `\n`, + }, + ], + data: { + quirksMode: false, + }, + type: `root`, + }) }, { pluginOptions: { excerpt_separator: `` } } ) diff --git a/packages/gatsby-transformer-remark/src/extend-node-type.js b/packages/gatsby-transformer-remark/src/extend-node-type.js index c4054ba1722aa..b115d90d0a462 100644 --- a/packages/gatsby-transformer-remark/src/extend-node-type.js +++ b/packages/gatsby-transformer-remark/src/extend-node-type.js @@ -45,6 +45,14 @@ const htmlAstCacheKey = node => `transformer-remark-markdown-html-ast-${ node.internal.contentDigest }-${pluginsCacheStr}-${pathPrefixCacheStr}` +const excerptCacheKey = (node, format) => + `transformer-remark-markdown-excerpt-${format}-${ + node.internal.contentDigest + }-${pluginsCacheStr}-${pathPrefixCacheStr}` +const excerptAstCacheKey = node => + `transformer-remark-markdown-excerpt-ast-${ + node.internal.contentDigest + }-${pluginsCacheStr}-${pathPrefixCacheStr}` const headingsCacheKey = node => `transformer-remark-markdown-headings-${ node.internal.contentDigest @@ -345,6 +353,108 @@ module.exports = ( } } + async function getExcerptAst( + markdownNode, + { pruneLength, truncate, excerptSeparator } + ) { + const cachedAst = await cache.get(excerptAstCacheKey(markdownNode)) + if (cachedAst) { + return cachedAst + } + + const fullAST = await getHTMLAst(markdownNode) + if (excerptSeparator) { + return cloneTreeUntil( + fullAST, + ({ nextNode }) => + nextNode.type === `raw` && nextNode.value === excerptSeparator + ) + } + if (!fullAST.children.length) { + cache.set(excerptAstCacheKey(markdownNode, fullAST)) + return fullAST + } + + const excerptAST = cloneTreeUntil(fullAST, ({ root }) => { + const totalExcerptSoFar = getConcatenatedValue(root) + return totalExcerptSoFar && totalExcerptSoFar.length > pruneLength + }) + const unprunedExcerpt = getConcatenatedValue(excerptAST) + if ( + !unprunedExcerpt || + (pruneLength && unprunedExcerpt.length < pruneLength) + ) { + cache.set(excerptAstCacheKey(markdownNode, excerptAST)) + return excerptAST + } + + const lastTextNode = findLastTextNode(excerptAST) + const amountToPruneLastNode = + pruneLength - (unprunedExcerpt.length - lastTextNode.value.length) + if (!truncate) { + lastTextNode.value = prune( + lastTextNode.value, + amountToPruneLastNode, + `…` + ) + } else { + lastTextNode.value = _.truncate(lastTextNode.value, { + length: pruneLength, + omission: `…`, + }) + } + cache.set(excerptAstCacheKey(markdownNode, excerptAST)) + return excerptAST + } + + async function getExcerpt( + markdownNode, + { format, pruneLength, truncate, excerptSeparator } + ) { + const cachedExcerpt = await cache.get( + excerptCacheKey(markdownNode, format) + ) + if (cachedExcerpt) { + return cachedExcerpt + } + + if (format === `html`) { + const excerptAST = await getExcerptAst(markdownNode, { + pruneLength, + truncate, + excerptSeparator, + }) + const html = hastToHTML(excerptAST, { + allowDangerousHTML: true, + }) + cache.set(excerptCacheKey(markdownNode, format), html) + return html + } + + if (markdownNode.excerpt) { + return markdownNode.excerpt + } + + const text = getAST(markdownNode).then(ast => { + const excerptNodes = [] + visit(ast, node => { + if (node.type === `text` || node.type === `inlineCode`) { + excerptNodes.push(node.value) + } + return + }) + if (!truncate) { + return prune(excerptNodes.join(` `), pruneLength, `…`) + } + return _.truncate(excerptNodes.join(` `), { + length: pruneLength, + omission: `…`, + }) + }) + cache.set(excerptCacheKey(markdownNode, format), text) + return text + } + const HeadingType = new GraphQLObjectType({ name: `MarkdownHeading`, fields: { @@ -415,77 +525,35 @@ module.exports = ( defaultValue: `plain`, }, }, - async resolve(markdownNode, { format, pruneLength, truncate }) { - if (format === `html`) { - if (pluginOptions.excerpt_separator) { - const fullAST = await getHTMLAst(markdownNode) - const excerptAST = cloneTreeUntil( - fullAST, - ({ nextNode }) => - nextNode.type === `raw` && - nextNode.value === pluginOptions.excerpt_separator - ) - return hastToHTML(excerptAST, { - allowDangerousHTML: true, - }) - } - const fullAST = await getHTMLAst(markdownNode) - if (!fullAST.children.length) { - return `` - } - - const excerptAST = cloneTreeUntil(fullAST, ({ root }) => { - const totalExcerptSoFar = getConcatenatedValue(root) - return totalExcerptSoFar && totalExcerptSoFar.length > pruneLength - }) - const unprunedExcerpt = getConcatenatedValue(excerptAST) - if (!unprunedExcerpt) { - return `` - } - - if (pruneLength && unprunedExcerpt.length < pruneLength) { - return hastToHTML(excerptAST, { - allowDangerousHTML: true, - }) - } - - const lastTextNode = findLastTextNode(excerptAST) - const amountToPruneLastNode = - pruneLength - (unprunedExcerpt.length - lastTextNode.value.length) - if (!truncate) { - lastTextNode.value = prune( - lastTextNode.value, - amountToPruneLastNode, - `…` - ) - } else { - lastTextNode.value = _.truncate(lastTextNode.value, { - length: pruneLength, - omission: `…`, - }) - } - return hastToHTML(excerptAST, { - allowDangerousHTML: true, - }) - } - if (markdownNode.excerpt) { - return Promise.resolve(markdownNode.excerpt) - } - return getAST(markdownNode).then(ast => { - const excerptNodes = [] - visit(ast, node => { - if (node.type === `text` || node.type === `inlineCode`) { - excerptNodes.push(node.value) - } - return - }) - if (!truncate) { - return prune(excerptNodes.join(` `), pruneLength, `…`) - } - return _.truncate(excerptNodes.join(` `), { - length: pruneLength, - omission: `…`, - }) + resolve(markdownNode, { format, pruneLength, truncate }) { + return getExcerpt(markdownNode, { + format, + pruneLength, + truncate, + excerptSeparator: pluginOptions.excerpt_separator, + }) + }, + }, + excerptAst: { + type: GraphQLJSON, + args: { + pruneLength: { + type: GraphQLInt, + defaultValue: 140, + }, + truncate: { + type: GraphQLBoolean, + defaultValue: false, + }, + }, + resolve(markdownNode, { pruneLength, truncate }) { + return getExcerptAst(markdownNode, { + pruneLength, + truncate, + excerptSeparator: pluginOptions.excerpt_separator, + }).then(ast => { + const strippedAst = stripPosition(_.clone(ast), true) + return hastReparseRaw(strippedAst) }) }, },