Skip to content

Commit

Permalink
Merge branch 'main' of github.com:quarto-dev/quarto-cli into main
Browse files Browse the repository at this point in the history
  • Loading branch information
jjallaire committed Apr 29, 2022
2 parents d94ba1d + c4b0511 commit 4c2f4b8
Show file tree
Hide file tree
Showing 18 changed files with 304 additions and 305 deletions.
1 change: 0 additions & 1 deletion src/core/handlers/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@

import "./mermaid.ts";
import "./include.ts";
import "./pagebreak.ts";
27 changes: 0 additions & 27 deletions src/core/handlers/pagebreak.ts

This file was deleted.

11 changes: 11 additions & 0 deletions src/core/url.ts
Original file line number Diff line number Diff line change
@@ -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, "/");
}
179 changes: 6 additions & 173 deletions src/project/types/website/listing/website-listing-feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -23,6 +23,7 @@ import {
websiteTitle,
} from "../website-config.ts";
import {
absoluteUrl,
kDescription,
kFieldAuthor,
kFieldCategories,
Expand All @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<string, RenderedContents> = {};
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;
Expand Down
79 changes: 73 additions & 6 deletions src/project/types/website/listing/website-listing-read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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 {
Expand Down Expand Up @@ -67,6 +69,7 @@ import {
ListingSharedOptions,
ListingSort,
ListingType,
renderedContentReader,
} from "./website-listing-shared.ts";
import {
kListingPageFieldAuthor,
Expand All @@ -81,9 +84,10 @@ 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";
import { projectOutputDir } from "../../../project-shared.ts";

// Defaults (a card listing that contains everything
// in the source document's directory)
Expand Down Expand Up @@ -249,6 +253,62 @@ 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 = join(projectOutputDir(context), relativePath);
const placeholder = descriptionPlaceholder(relativePath);
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;
Deno.writeTextFileSync(
outputFile.file,
fileContents,
);
}
});
}

function descriptionPlaceholder(file?: string): string {
return file ? `<!-- desc(5A0113B34292):${file} -->` : "";
}

const descriptionPlaceholderRegex = /<!-- desc\(5A0113B34292\):(.*) -->/;

function hydrateListing(
format: Format,
listing: ListingDehydrated,
Expand Down Expand Up @@ -559,6 +619,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
Expand All @@ -569,7 +635,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
Expand Down
Loading

0 comments on commit 4c2f4b8

Please sign in to comment.