From e47e54f17149e26f3e6510236ba15b48231b9106 Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Thu, 28 Apr 2022 15:49:22 -0700 Subject: [PATCH 1/5] revert pagebreak shortcode --- src/core/handlers/handlers.ts | 1 - src/core/handlers/pagebreak.ts | 27 ------------- .../filters/quarto-pre/quarto-pre.lua | 2 - .../{quarto-pre => rmarkdown}/pagebreak.lua | 14 +++---- src/resources/rmd/execute.R | 2 +- tests/smoke/directives/pagebreak.test.ts | 40 ------------------- 6 files changed, 7 insertions(+), 79 deletions(-) delete mode 100644 src/core/handlers/pagebreak.ts rename src/resources/filters/{quarto-pre => rmarkdown}/pagebreak.lua (94%) delete mode 100644 tests/smoke/directives/pagebreak.test.ts diff --git a/src/core/handlers/handlers.ts b/src/core/handlers/handlers.ts index 544b93c10b..0ee53523ff 100644 --- a/src/core/handlers/handlers.ts +++ b/src/core/handlers/handlers.ts @@ -7,4 +7,3 @@ import "./mermaid.ts"; import "./include.ts"; -import "./pagebreak.ts"; diff --git a/src/core/handlers/pagebreak.ts b/src/core/handlers/pagebreak.ts deleted file mode 100644 index b28c96a1ee..0000000000 --- a/src/core/handlers/pagebreak.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* -* page-break.ts -* -* Copyright (C) 2022 by RStudio, PBC -* -*/ - -import { LanguageCellHandlerContext, LanguageHandler } from "./types.ts"; -import { baseHandler, install } from "./base.ts"; -import { DirectiveCell } from "../lib/break-quarto-md-types.ts"; - -const includeHandler: LanguageHandler = { - ...baseHandler, - - languageName: "pagebreak", - type: "directive", - stage: "pre-engine", - - directive( - _handlerContext: LanguageCellHandlerContext, - _directive: DirectiveCell, - ) { - return "\n\n\\pagebreak\n\n"; - }, -}; - -install(includeHandler); diff --git a/src/resources/filters/quarto-pre/quarto-pre.lua b/src/resources/filters/quarto-pre/quarto-pre.lua index 5b365730ff..cc4be0d9bc 100644 --- a/src/resources/filters/quarto-pre/quarto-pre.lua +++ b/src/resources/filters/quarto-pre/quarto-pre.lua @@ -74,7 +74,6 @@ import("hidden.lua") import("line-numbers.lua") import("output-location.lua") import("include-paths.lua") -import("pagebreak.lua") -- [/import] initParams() @@ -106,7 +105,6 @@ return { panelTabset(), panelLayout(), panelSidebar(), - pageBreaks(), }), combineFilters({ fileMetadata(), diff --git a/src/resources/filters/quarto-pre/pagebreak.lua b/src/resources/filters/rmarkdown/pagebreak.lua similarity index 94% rename from src/resources/filters/quarto-pre/pagebreak.lua rename to src/resources/filters/rmarkdown/pagebreak.lua index f0d5323c6f..0177a4cca9 100644 --- a/src/resources/filters/quarto-pre/pagebreak.lua +++ b/src/resources/filters/rmarkdown/pagebreak.lua @@ -78,9 +78,9 @@ function RawBlock (el) if FORMAT:match 'tex$' then return nil end - -- check that the block contains only + -- check that the block is TeX or LaTeX and contains only -- \newpage or \pagebreak. - if is_newpage_command(el.text) then + if el.format:match 'tex' and is_newpage_command(el.text) then -- use format-specific pagebreak marker. FORMAT is set by pandoc to -- the targeted output format. return newpage(FORMAT) @@ -97,9 +97,7 @@ function Para (el) end end -function pageBreaks() - return { - Meta = pagebreaks_from_config, - RawBlock = RawBlock, Para = Para - } -end +return { + {Meta = pagebreaks_from_config}, + {RawBlock = RawBlock, Para = Para} +} diff --git a/src/resources/rmd/execute.R b/src/resources/rmd/execute.R index 5b01de9d50..dfacf3a58d 100644 --- a/src/resources/rmd/execute.R +++ b/src/resources/rmd/execute.R @@ -157,7 +157,7 @@ execute <- function(input, format, tempDir, libDir, dependencies, cwd, params, r list( markdown = paste(markdown, collapse="\n"), supporting = I(supporting), - filters = as.character(c()), + filters = I("rmarkdown/pagebreak.lua"), includes = includes, engineDependencies = engineDependencies, preserve = preserve, diff --git a/tests/smoke/directives/pagebreak.test.ts b/tests/smoke/directives/pagebreak.test.ts deleted file mode 100644 index 0016ef4311..0000000000 --- a/tests/smoke/directives/pagebreak.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* -* pagebreak.test.ts -* -* Copyright (C) 2021 by RStudio, PBC -* -*/ -import { join } from "path/mod.ts"; - -import { ensureHtmlSelectorSatisfies } from "../../verify.ts"; - -import { renderVerifyLatexOutput, testRender } from "../render/render.ts"; - -import { fileLoader } from "../../utils.ts"; -import { Element } from "../../../src/core/deno-dom.ts"; -import { dirAndStem } from "../../../src/core/path.ts"; - -const directives = fileLoader("directives"); - -const test1 = directives("pagebreak/minimal.qmd", "html"); -testRender(test1.input, "html", false, [ - ensureHtmlSelectorSatisfies(test1.output.outputPath, "div", (nodeList) => { - const nodes = Array.from(nodeList); - - return nodes.filter((node) => - (node as Element).getAttribute("style") === "page-break-after: always;" - ).length === 2; - }), -], { - teardown: () => { - const [dir, stem] = dirAndStem(test1.input); - Deno.removeSync(join(dir, `${stem}.md`)); - return Promise.resolve(); - }, -}); - -const test2 = directives("pagebreak/one-break.qmd", "latex"); -renderVerifyLatexOutput(test2.input, [/\\pagebreak/]); - -const test3 = directives("pagebreak/one-raw-break.qmd", "latex"); -renderVerifyLatexOutput(test3.input, [/\\pagebreak/]); From 1d18844fa0e16af5f350bd006b5479f03cdebe6b Mon Sep 17 00:00:00 2001 From: Charles Teague Date: Thu, 28 Apr 2022 18:00:37 -0400 Subject: [PATCH 2/5] Improve Listing Description Default Behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We were previously reading the description for listings (if a description or abstract wasn’t provided) by looking at the source file’s markdown. This is rife with issues trying to find the proper content. Instead, allow the pages to render and then actually fetch the first paragraph out of the rendered content (and use that!). --- .../website/listing/website-listing-feed.ts | 179 +---------------- .../website/listing/website-listing-read.ts | 67 ++++++- .../website/listing/website-listing-shared.ts | 185 ++++++++++++++++++ .../types/website/listing/website-listing.ts | 8 +- .../types/website/util/discover-meta.ts | 35 ---- .../website/listing/quarto-listing.scss | 4 + 6 files changed, 266 insertions(+), 212 deletions(-) diff --git a/src/project/types/website/listing/website-listing-feed.ts b/src/project/types/website/listing/website-listing-feed.ts index 891164dd26..a600885fc3 100644 --- a/src/project/types/website/listing/website-listing-feed.ts +++ b/src/project/types/website/listing/website-listing-feed.ts @@ -5,9 +5,9 @@ * */ -import { dirname, join, relative } from "path/mod.ts"; +import { join, relative } from "path/mod.ts"; import { warning } from "log/mod.ts"; -import { Document, DOMParser, Element } from "deno_dom/deno-dom-wasm-noinit.ts"; +import { Document } from "deno_dom/deno-dom-wasm-noinit.ts"; import { uniqBy } from "../../../../core/lodash.ts"; import { Format } from "../../../../config/types.ts"; @@ -23,6 +23,7 @@ import { websiteTitle, } from "../website-config.ts"; import { + absoluteUrl, kDescription, kFieldAuthor, kFieldCategories, @@ -32,13 +33,12 @@ import { ListingDescriptor, ListingFeedOptions, ListingItem, + renderedContentReader, + RenderedContents, } from "./website-listing-shared.ts"; import { dirAndStem, resolvePathGlobs } from "../../../../core/path.ts"; import { ProjectOutputFile } from "../../types.ts"; import { resolveInputTarget } from "../../../project-index.ts"; -import { - defaultSyntaxHighlightingClassMap, -} from "../../../../command/render/pandoc-html.ts"; import { projectOutputDir } from "../../../project-shared.ts"; import { imageContentType, imageSize } from "../../../../core/image.ts"; import { warnOnce } from "../../../../core/log.ts"; @@ -247,7 +247,7 @@ export function completeStagedFeeds( // Any feed files for this output file const files = resolvePathGlobs(dir, [`${stem}${kStagedFileGlob}`], []); - const contentReader = renderedContentReader(context, siteUrl!); + const contentReader = renderedContentReader(context, true, siteUrl); for (const feedFile of files.include) { // Info about this feed file @@ -545,173 +545,6 @@ function feedPath(dir: string, stem: string, full: boolean) { return join(dir, file); } -interface RenderedContents { - title: string | undefined; - firstPara: string | undefined; - fullContents: string | undefined; -} - -const renderedContentReader = (project: ProjectContext, siteUrl: string) => { - const renderedContent: Record = {}; - return (filePath: string): RenderedContents => { - if (!renderedContent[filePath]) { - renderedContent[filePath] = readRenderedContents( - filePath, - siteUrl, - project, - ); - } - return renderedContent[filePath]; - }; -}; - -// This reads a rendered HTML file and extracts its contents. -// The contents will be cleaned to make them conformant to any -// RSS validators (I used W3 validator to identify problematic HTML) -function readRenderedContents( - filePath: string, - siteUrl: string, - project: ProjectContext, -): RenderedContents { - const htmlInput = Deno.readTextFileSync(filePath); - const doc = new DOMParser().parseFromString(htmlInput, "text/html")!; - - const fileRelPath = relative(projectOutputDir(project), filePath); - const fileRelFolder = dirname(fileRelPath); - - const mainEl = doc.querySelector("main.content"); - - // Capture the rendered title and remove it from the content - const titleEl = doc.getElementById("title-block-header"); - const titleText = titleEl?.querySelector("h1.title")?.innerText; - if (titleEl) { - titleEl.remove(); - } - - // Remove any navigation elements from the content region - const navEls = doc.querySelectorAll("nav"); - if (navEls) { - for (const navEl of navEls) { - navEl.remove(); - } - } - - // Convert any images to have absolute paths - const imgNodes = doc.querySelectorAll("img"); - if (imgNodes) { - for (const imgNode of imgNodes) { - const imgEl = imgNode as Element; - let src = imgEl.getAttribute("src"); - if (src) { - if (!src.startsWith("/")) { - src = join(fileRelFolder, src); - } - imgEl.setAttribute("src", absoluteUrl(siteUrl, src)); - } - } - } - - // Strip unacceptable elements - const stripSelectors = [ - '*[aria-hidden="true"]', // Feeds should not contain aria hidden elements - "button.code-copy-button", // Code copy buttons looks weird and don't work - ]; - stripSelectors.forEach((sel) => { - const nodes = doc.querySelectorAll(sel); - nodes?.forEach((node) => { - node.remove(); - }); - }); - - // Strip unacceptable attributes - const stripAttrs = [ - "role", - ]; - stripAttrs.forEach((attr) => { - const nodes = doc.querySelectorAll(`[${attr}]`); - nodes?.forEach((node) => { - const el = node as Element; - el.removeAttribute(attr); - }); - }); - - // String unacceptable links - const relativeLinkSel = 'a[href^="#"]'; - const linkNodes = doc.querySelectorAll(relativeLinkSel); - linkNodes.forEach((linkNode) => { - const nodesToMove = linkNode.childNodes; - linkNode.after(...nodesToMove); - linkNode.remove(); - }); - - // Process code to apply styles for syntax highlighting - const highlightingMap = defaultSyntaxHighlightingClassMap(); - const spanNodes = doc.querySelectorAll("code span"); - for (const spanNode of spanNodes) { - const spanEl = spanNode as Element; - - for (const clz of spanEl.classList) { - const styles = highlightingMap[clz]; - if (styles) { - spanEl.setAttribute("style", styles.join("\n")); - break; - } - } - } - - // Apply a code background color - const codeStyle = "background: #f1f3f5;"; - const codeBlockNodes = doc.querySelectorAll("div.sourceCode"); - for (const codeBlockNode of codeBlockNodes) { - const codeBlockEl = codeBlockNode as Element; - codeBlockEl.setAttribute("style", codeStyle); - } - - // Process math using webtex - const trimMath = (str: string) => { - // Text of math is prefixed by the below - if (str.length > 4 && (str.startsWith("\\[") || str.startsWith("\\("))) { - const trimStart = str.slice(2); - return trimStart.slice(0, trimStart.length - 2); - } else { - return str; - } - }; - const mathNodes = doc.querySelectorAll("span.math"); - for (const mathNode of mathNodes) { - const mathEl = mathNode as Element; - const math = trimMath(mathEl.innerText); - const imgEl = doc.createElement("IMG"); - imgEl.setAttribute( - "src", - kWebTexUrl(math), - ); - mathNode.parentElement?.replaceChild(imgEl, mathNode); - } - - return { - title: titleText, - fullContents: mainEl?.innerHTML, - firstPara: mainEl?.querySelector("p")?.innerHTML, - }; -} - -const kWebTexUrl = ( - math: string, - type: "png" | "svg" | "gif" | "emf" | "pdf" = "png", -) => { - const encodedMath = encodeURI(math); - return `https://latex.codecogs.com/${type}.latex?${encodedMath}`; -}; - -const absoluteUrl = (siteUrl: string, url: string) => { - if (url.startsWith("http:") || url.startsWith("https:")) { - return url; - } else { - return `${siteUrl}/${url}`; - } -}; - // See https://validator.w3.org/feed/docs/rss2.html#ltimagegtSubelementOfLtchannelgt const kMaxWidth = 144; const kMaxHeight = 400; diff --git a/src/project/types/website/listing/website-listing-read.ts b/src/project/types/website/listing/website-listing-read.ts index ad06484503..feb35091b5 100644 --- a/src/project/types/website/listing/website-listing-read.ts +++ b/src/project/types/website/listing/website-listing-read.ts @@ -16,12 +16,14 @@ import { pathWithForwardSlashes, resolvePathGlobs, } from "../../../../core/path.ts"; -import { inputTargetIndex } from "../../../project-index.ts"; +import { + inputTargetIndex, + resolveInputTarget, +} from "../../../project-index.ts"; import { ProjectContext } from "../../../types.ts"; import { estimateReadingTimeMinutes, - findDescriptionMd, findPreviewImgMd, } from "../util/discover-meta.ts"; import { @@ -67,6 +69,7 @@ import { ListingSharedOptions, ListingSort, ListingType, + renderedContentReader, } from "./website-listing-shared.ts"; import { kListingPageFieldAuthor, @@ -84,6 +87,8 @@ import { isYamlPath, readYaml } from "../../../../core/yaml.ts"; import { projectYamlFiles } from "../../../project-context.ts"; import { parseAuthor } from "../../../../core/author.ts"; import { parsePandocDate, resolveDate } from "../../../../core/date.ts"; +import { ProjectOutputFile } from "../../types.ts"; +import { projectOutputDir } from "../../../project-shared.ts"; // Defaults (a card listing that contains everything // in the source document's directory) @@ -249,6 +254,55 @@ export async function readListings( return { listingDescriptors: listingItems, options: sharedOptions }; } +export function completeListingDescriptions( + context: ProjectContext, + outputFiles: ProjectOutputFile[], + _incremental: boolean, +) { + const contentReader = renderedContentReader(context, false); + + // Go through any output files and fix up any feeds associated with them + outputFiles.forEach((outputFile) => { + // Does this output file contain a listing? + if (outputFile.format.metadata[kListing]) { + // Read the listing page + let fileContents = Deno.readTextFileSync(outputFile.file); + + // Use a regex to identify any placeholders + const regex = descriptionPlaceholderRegex; + regex.lastIndex = 0; + let match = regex.exec(fileContents); + while (match) { + // For each placeholder, get its target href, then read the contents of that + // file and inject the contents. + const relativePath = match[1]; + const absolutePath = relativePath.startsWith("/") + ? join(projectOutputDir(context), relativePath) + : join(dirname(outputFile.file), relativePath); + const contents = contentReader(absolutePath); + const placeholder = descriptionPlaceholder(relativePath); + fileContents = fileContents.replace( + placeholder, + contents.firstPara || "", + ); + + match = regex.exec(fileContents); + } + regex.lastIndex = 0; + Deno.writeTextFileSync( + outputFile.file, + fileContents, + ); + } + }); +} + +function descriptionPlaceholder(file?: string): string { + return file ? `` : ""; +} + +const descriptionPlaceholderRegex = //; + function hydrateListing( format: Format, listing: ListingDehydrated, @@ -559,6 +613,12 @@ async function listItemFromFile(input: string, project: ProjectContext) { project, projectRelativePath, ); + const inputTarget = await resolveInputTarget( + project, + projectRelativePath, + false, + ); + const documentMeta = target?.markdown.yaml; if (documentMeta?.draft) { // This is a draft, don't include it in the listing @@ -569,7 +629,8 @@ async function listItemFromFile(input: string, project: ProjectContext) { const filemodified = fileModifiedDate(input); const description = documentMeta?.description as string || documentMeta?.abstract as string || - findDescriptionMd(target?.markdown.markdown); + descriptionPlaceholder(inputTarget?.outputHref); + const imageRaw = documentMeta?.image as string || findPreviewImgMd(target?.markdown.markdown); const image = imageRaw !== undefined diff --git a/src/project/types/website/listing/website-listing-shared.ts b/src/project/types/website/listing/website-listing-shared.ts index 199bdcdefc..96f7195d2f 100644 --- a/src/project/types/website/listing/website-listing-shared.ts +++ b/src/project/types/website/listing/website-listing-shared.ts @@ -5,10 +5,18 @@ * Copyright (C) 2020 by RStudio, PBC * */ +import { dirname, join, relative } from "path/mod.ts"; +import { DOMParser, Element } from "deno_dom/deno-dom-wasm-noinit.ts"; + +import { + defaultSyntaxHighlightingClassMap, +} from "../../../../command/render/pandoc-html.ts"; import { kTitle } from "../../../../config/constants.ts"; import { Metadata } from "../../../../config/types.ts"; +import { ProjectContext } from "../../../types.ts"; import { kImage } from "../website-constants.ts"; +import { projectOutputDir } from "../../../project-shared.ts"; // The root listing key export const kListing = "listing"; @@ -185,3 +193,180 @@ export interface ListingItem extends Record { [kFieldFileModified]?: Date; sortableValues?: Record; } + +export interface RenderedContents { + title: string | undefined; + firstPara: string | undefined; + fullContents: string | undefined; +} + +export const renderedContentReader = ( + project: ProjectContext, + forFeed: boolean, + siteUrl?: string, +) => { + const renderedContent: Record = {}; + return (filePath: string): RenderedContents => { + if (!renderedContent[filePath]) { + renderedContent[filePath] = readRenderedContents( + filePath, + project, + forFeed, + siteUrl, + ); + } + return renderedContent[filePath]; + }; +}; + +export const absoluteUrl = (siteUrl: string, url: string) => { + if (url.startsWith("http:") || url.startsWith("https:")) { + return url; + } else { + return `${siteUrl}/${url}`; + } +}; + +// This reads a rendered HTML file and extracts its contents. +// The contents will be cleaned to make them conformant to any +// RSS validators (I used W3 validator to identify problematic HTML) +export function readRenderedContents( + filePath: string, + project: ProjectContext, + forFeed: boolean, + siteUrl?: string, +): RenderedContents { + const htmlInput = Deno.readTextFileSync(filePath); + const doc = new DOMParser().parseFromString(htmlInput, "text/html")!; + + const fileRelPath = relative(projectOutputDir(project), filePath); + const fileRelFolder = dirname(fileRelPath); + + const mainEl = doc.querySelector("main.content"); + + // Capture the rendered title and remove it from the content + const titleEl = doc.getElementById("title-block-header"); + const titleText = titleEl?.querySelector("h1.title")?.innerText; + if (titleEl) { + titleEl.remove(); + } + + // Remove any navigation elements from the content region + const navEls = doc.querySelectorAll("nav"); + if (navEls) { + for (const navEl of navEls) { + navEl.remove(); + } + } + + // Convert any images to have absolute paths + if (forFeed && siteUrl) { + const imgNodes = doc.querySelectorAll("img"); + if (imgNodes) { + for (const imgNode of imgNodes) { + const imgEl = imgNode as Element; + let src = imgEl.getAttribute("src"); + if (src) { + if (!src.startsWith("/")) { + src = join(fileRelFolder, src); + } + imgEl.setAttribute("src", absoluteUrl(siteUrl, src)); + } + } + } + } + + // Strip unacceptable elements + const stripSelectors = [ + '*[aria-hidden="true"]', // Feeds should not contain aria hidden elements + "button.code-copy-button", // Code copy buttons looks weird and don't work + ]; + stripSelectors.forEach((sel) => { + const nodes = doc.querySelectorAll(sel); + nodes?.forEach((node) => { + node.remove(); + }); + }); + + // Strip unacceptable attributes + const stripAttrs = [ + "role", + ]; + stripAttrs.forEach((attr) => { + const nodes = doc.querySelectorAll(`[${attr}]`); + nodes?.forEach((node) => { + const el = node as Element; + el.removeAttribute(attr); + }); + }); + + // String unacceptable links + const relativeLinkSel = 'a[href^="#"]'; + const linkNodes = doc.querySelectorAll(relativeLinkSel); + linkNodes.forEach((linkNode) => { + const nodesToMove = linkNode.childNodes; + linkNode.after(...nodesToMove); + linkNode.remove(); + }); + + if (forFeed) { + // Process code to apply styles for syntax highlighting + const highlightingMap = defaultSyntaxHighlightingClassMap(); + const spanNodes = doc.querySelectorAll("code span"); + for (const spanNode of spanNodes) { + const spanEl = spanNode as Element; + + for (const clz of spanEl.classList) { + const styles = highlightingMap[clz]; + if (styles) { + spanEl.setAttribute("style", styles.join("\n")); + break; + } + } + } + + // Apply a code background color + const codeStyle = "background: #f1f3f5;"; + const codeBlockNodes = doc.querySelectorAll("div.sourceCode"); + for (const codeBlockNode of codeBlockNodes) { + const codeBlockEl = codeBlockNode as Element; + codeBlockEl.setAttribute("style", codeStyle); + } + + // Process math using webtex + const trimMath = (str: string) => { + // Text of math is prefixed by the below + if (str.length > 4 && (str.startsWith("\\[") || str.startsWith("\\("))) { + const trimStart = str.slice(2); + return trimStart.slice(0, trimStart.length - 2); + } else { + return str; + } + }; + const mathNodes = doc.querySelectorAll("span.math"); + for (const mathNode of mathNodes) { + const mathEl = mathNode as Element; + const math = trimMath(mathEl.innerText); + const imgEl = doc.createElement("IMG"); + imgEl.setAttribute( + "src", + kWebTexUrl(math), + ); + mathNode.parentElement?.replaceChild(imgEl, mathNode); + } + } + + return { + title: titleText, + fullContents: mainEl?.innerHTML, + firstPara: mainEl?.querySelector("p")?.innerHTML, + }; +} + +const kWebTexUrl = ( + math: string, + type: "png" | "svg" | "gif" | "emf" | "pdf" = "png", +) => { + const encodedMath = encodeURI(math); + return `https://latex.codecogs.com/${type}.latex?${encodedMath}`; +}; diff --git a/src/project/types/website/listing/website-listing.ts b/src/project/types/website/listing/website-listing.ts index 199bff2332..e7ddb6d0c2 100644 --- a/src/project/types/website/listing/website-listing.ts +++ b/src/project/types/website/listing/website-listing.ts @@ -44,7 +44,10 @@ import { templateJsScript, templateMarkdownHandler, } from "./website-listing-template.ts"; -import { readListings } from "./website-listing-read.ts"; +import { + completeListingDescriptions, + readListings, +} from "./website-listing-read.ts"; import { categorySidebar } from "./website-listing-categories.ts"; import { TempContext } from "../../../../core/temp.ts"; import { completeStagedFeeds, createFeed } from "./website-listing-feed.ts"; @@ -283,6 +286,9 @@ export function completeListingGeneration( // Complete any staged feeds completeStagedFeeds(context, outputFiles, incremental); + // Ensure any listing items have their rendered descriptions populated + completeListingDescriptions(context, outputFiles, incremental); + // Write a global listing index updateGlobalListingIndex(context, outputFiles, incremental); } diff --git a/src/project/types/website/util/discover-meta.ts b/src/project/types/website/util/discover-meta.ts index 7458ae71e5..0449c1d95f 100644 --- a/src/project/types/website/util/discover-meta.ts +++ b/src/project/types/website/util/discover-meta.ts @@ -8,7 +8,6 @@ import { Document, Element } from "deno_dom/deno-dom-wasm-noinit.ts"; import { getDecodedAttribute } from "../../../../core/html.ts"; -import { lines } from "../../../../core/text.ts"; // Image discovery happens by either: // Finding an image with the class 'preview-image' @@ -95,37 +94,3 @@ export function findPreviewImgMd(markdown?: string): string | undefined { } return undefined; } - -export function findDescriptionMd(markdown?: string): string | undefined { - if (markdown) { - const previewText: string[] = []; - let accum = false; - - // Controls what counts as ignorable lines (empty or markdown of - // specific types) - const skipLines = [/^\#+/, /^\:\:\:[\:]*/]; - const emptyLine = (line: string) => { - return line.trim() === ""; - }; - - // Go through each line and find the first paragraph, then accumulate - // that text as the description - for (const line of lines(markdown)) { - if (!accum) { - // When we encounter the first - if (!emptyLine(line) && !skipLines.find((skip) => line.match(skip))) { - accum = true; - previewText.push(line); - } - } else { - if (emptyLine(line) || skipLines.find((skip) => line.match(skip))) { - break; - } else { - previewText.push(line); - } - } - } - return previewText.join("\n"); - } - return undefined; -} diff --git a/src/resources/projects/website/listing/quarto-listing.scss b/src/resources/projects/website/listing/quarto-listing.scss index e9b45094cf..679879da3f 100644 --- a/src/resources/projects/website/listing/quarto-listing.scss +++ b/src/resources/projects/website/listing/quarto-listing.scss @@ -632,4 +632,8 @@ div.quarto-post { .listing-categories { @include listing-category(); } + + .listing-description { + margin-bottom: 0.5em; + } } From e7e7129b2beea514f6988ac68ad5bf061613728f Mon Sep 17 00:00:00 2001 From: Charles Teague Date: Thu, 28 Apr 2022 18:33:15 -0400 Subject: [PATCH 3/5] warn on failure to read description listing --- .../website/listing/website-listing-read.ts | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/project/types/website/listing/website-listing-read.ts b/src/project/types/website/listing/website-listing-read.ts index feb35091b5..04c0c8b836 100644 --- a/src/project/types/website/listing/website-listing-read.ts +++ b/src/project/types/website/listing/website-listing-read.ts @@ -5,8 +5,8 @@ * Copyright (C) 2020 by RStudio, PBC * */ - -import { basename, dirname, isGlob, join, relative } from "path/mod.ts"; +import { warning } from "log/mod.ts"; +import { basename, dirname, join, relative } from "path/mod.ts"; import { cloneDeep, orderBy } from "../../../../core/lodash.ts"; import { existsSync } from "fs/mod.ts"; @@ -84,7 +84,6 @@ import { } from "../../../../config/constants.ts"; import { isAbsoluteRef } from "../../../../core/http.ts"; import { isYamlPath, readYaml } from "../../../../core/yaml.ts"; -import { projectYamlFiles } from "../../../project-context.ts"; import { parseAuthor } from "../../../../core/author.ts"; import { parsePandocDate, resolveDate } from "../../../../core/date.ts"; import { ProjectOutputFile } from "../../types.ts"; @@ -276,16 +275,23 @@ export function completeListingDescriptions( // For each placeholder, get its target href, then read the contents of that // file and inject the contents. const relativePath = match[1]; - const absolutePath = relativePath.startsWith("/") - ? join(projectOutputDir(context), relativePath) - : join(dirname(outputFile.file), relativePath); - const contents = contentReader(absolutePath); + const absolutePath = join(projectOutputDir(context), relativePath); const placeholder = descriptionPlaceholder(relativePath); - fileContents = fileContents.replace( - placeholder, - contents.firstPara || "", - ); - + if (existsSync(absolutePath)) { + const contents = contentReader(absolutePath); + fileContents = fileContents.replace( + placeholder, + contents.firstPara || "", + ); + } else { + fileContents = fileContents.replace( + placeholder, + "", + ); + warning( + `Unable to read listing item description from ${relativePath}`, + ); + } match = regex.exec(fileContents); } regex.lastIndex = 0; From 07f37b0395bfc4b4290ec0995c40bda86618f5ca Mon Sep 17 00:00:00 2001 From: Charles Teague Date: Thu, 28 Apr 2022 18:33:30 -0400 Subject: [PATCH 4/5] Update tests test --- tests/smoke/site/render-blog.test.ts | 4 ++-- tests/smoke/{render => site}/render-listings.test.ts | 6 ++---- tests/smoke/site/render-navigation.test.ts | 6 +++--- tests/smoke/site/site.ts | 3 ++- 4 files changed, 9 insertions(+), 10 deletions(-) rename tests/smoke/{render => site}/render-listings.test.ts (91%) diff --git a/tests/smoke/site/render-blog.test.ts b/tests/smoke/site/render-blog.test.ts index c97802eb2d..39be1616e9 100644 --- a/tests/smoke/site/render-blog.test.ts +++ b/tests/smoke/site/render-blog.test.ts @@ -7,13 +7,13 @@ import { docs } from "../../utils.ts"; import { testSite } from "./site.ts"; -testSite(docs("blog/about.qmd"), [ +testSite(docs("blog/about.qmd"), docs("blog/about.qmd"), [ "div.quarto-about-jolla", // Correct type "img.about-image", // Image is present "div.about-links", // Links are present "main.content", // Main content is still there ], []); -testSite(docs("blog/index.qmd"), [ +testSite(docs("blog/index.qmd"), docs("blog"), [ "div#listing-listing", // the listing is rendered there "div.list.quarto-listing-default", // The correct type of listing ".quarto-listing-category-title", // Categories are present diff --git a/tests/smoke/render/render-listings.test.ts b/tests/smoke/site/render-listings.test.ts similarity index 91% rename from tests/smoke/render/render-listings.test.ts rename to tests/smoke/site/render-listings.test.ts index 2a486bba3c..fb9e34d0e5 100644 --- a/tests/smoke/render/render-listings.test.ts +++ b/tests/smoke/site/render-listings.test.ts @@ -4,7 +4,7 @@ * Copyright (C) 2020 by RStudio, PBC * */ -import { join } from "path/mod.ts"; +import { dirname, join } from "path/mod.ts"; import { testQuartoCmd, Verify } from "../../test.ts"; import { docs } from "../../utils.ts"; @@ -27,9 +27,7 @@ verify.push(ensureHtmlElements(htmlOutput, [ testQuartoCmd( "render", [ - input, - "--to", - "html", + dirname(input), ], verify, { diff --git a/tests/smoke/site/render-navigation.test.ts b/tests/smoke/site/render-navigation.test.ts index 170d52bbb4..74ee026f55 100644 --- a/tests/smoke/site/render-navigation.test.ts +++ b/tests/smoke/site/render-navigation.test.ts @@ -8,19 +8,19 @@ import { docs } from "../../utils.ts"; import { testSite } from "./site.ts"; // Test a page with page navigation -testSite(docs("site-navigation/page2.qmd"), [ +testSite(docs("site-navigation/page2.qmd"), docs("site-navigation/page2.qmd"), [ ".page-navigation .nav-page-next a .nav-page-text", // Next page target and text ".page-navigation .nav-page-previous a .nav-page-text", // Prev page target and text ], []); // Test a page with only previous nav -testSite(docs("site-navigation/page3.qmd"), [ +testSite(docs("site-navigation/page3.qmd"), docs("site-navigation/page3.qmd"), [ ".page-navigation .nav-page-previous a .nav-page-text", // Prev page target and text ], [ ".page-navigation .nav-page-next a .nav-page-text", // Next page target and text ]); -testSite(docs("site-navigation/index.qmd"), [ +testSite(docs("site-navigation/index.qmd"), docs("site-navigation/index.qmd"), [ ".navbar .nav-item", // Navbar with nav item ".navbar #quarto-search", // Search is present on navbar "#quarto-sidebar .sidebar-item a", // The sidebar with items is present diff --git a/tests/smoke/site/site.ts b/tests/smoke/site/site.ts index 57c886fc05..54c3212338 100644 --- a/tests/smoke/site/site.ts +++ b/tests/smoke/site/site.ts @@ -12,6 +12,7 @@ import { ensureHtmlElements, noErrorsOrWarnings } from "../../verify.ts"; export const testSite = ( input: string, + renderTarget: string, includeSelectors: string[], excludeSelectors: string[], ) => { @@ -26,7 +27,7 @@ export const testSite = ( // Run the command testQuartoCmd( "render", - [input], + [renderTarget], [noErrorsOrWarnings, verifySel], { teardown: async () => { From c4b051106b84306415512eeca086746c4da75950 Mon Sep 17 00:00:00 2001 From: Charles Teague Date: Thu, 28 Apr 2022 21:19:36 -0400 Subject: [PATCH 5/5] Properly join url for website meta --- src/core/url.ts | 11 +++++++++++ src/project/types/website/website-meta.ts | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 src/core/url.ts diff --git a/src/core/url.ts b/src/core/url.ts new file mode 100644 index 0000000000..35f1625a31 --- /dev/null +++ b/src/core/url.ts @@ -0,0 +1,11 @@ +/* +* url.ts +* +* Copyright (C) 2020 by RStudio, PBC +* +*/ + +export function joinUrl(baseUrl: string, path: string) { + const joined = `${baseUrl}/${path}`; + return joined.replace(/\/\//g, "/"); +} diff --git a/src/project/types/website/website-meta.ts b/src/project/types/website/website-meta.ts index b87d952cb6..b12ad99771 100644 --- a/src/project/types/website/website-meta.ts +++ b/src/project/types/website/website-meta.ts @@ -44,6 +44,7 @@ import { import { HtmlPostProcessResult } from "../../../command/render/types.ts"; import { imageSize } from "../../../core/image.ts"; import { writeMetaTag } from "../../../format/html/format-html-shared.ts"; +import { joinUrl } from "../../../core/url.ts"; const kCard = "card"; @@ -333,7 +334,7 @@ function imageMetadata( // resolve the image path into an absolute href return { - href: `${baseUrl}/${imageProjectRelative}`, + href: joinUrl(baseUrl, imageProjectRelative), height: size?.height, width: size?.width, };