Path: blob/main/src/quarto-core/attribution/document.ts
3583 views
/*1* document.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/56import {7basename,8dirname,9isAbsolute,10join,11relative,12} from "../../deno_ral/path.ts";13import {14kAbstract,15kAuthor,16kAuthors,17kCsl,18kLang,19kTitle,20} from "../../config/constants.ts";21import { Format, Metadata } from "../../config/types.ts";22import { parseAuthor } from "../../core/author.ts";23import { normalizePath, pathWithForwardSlashes } from "../../core/path.ts";24import {25CSL,26cslDate,27CSLExtras,28cslNames,29CSLType,30cslType,31kAbstractUrl,32kEIssn,33kPdfUrl,34suggestId,35} from "../../core/csl.ts";36import {37kSiteUrl,38kWebsite,39} from "../../project/types/website/website-constants.ts";40import { resolveAndFormatDate } from "../../core/date.ts";4142const kDOI = "DOI";43const kCitation = "citation";44const kURL = "URL";45const kId = "id";46const kCitationKey = "citation-key";47const kEditor = "editor";4849const kType = "type";50const kCategories = "categories";51const kLanguage = "language";52const kAvailableDate = "available-date";53const kIssued = "issued";54const kDate = "date";5556const kPublisher = "publisher";57const kContainerTitle = "container-title";58const kVolume = "volume";59const kIssue = "issue";60const kISSN = "issn";61const kISBN = "isbn";62const kPMCID = "pmcid";63const kPMID = "pmid";64const kFirstPage = "page-first";65const kLastPage = "page-last";66const kPage = "page";67const kNumber = "number";68const kCustom = "custom";69const kArchiveCollection = "archive_collection";70const kArchiveLocation = "archive_location";7172// Provides an absolute path to the referenced CSL file73export const getCSLPath = (input: string, format: Format) => {74const cslPath = format.metadata[kCsl] as string;75if (cslPath) {76if (isAbsolute(cslPath)) {77return cslPath;78} else {79return join(dirname(input), cslPath);80}81} else {82return undefined;83}84};8586// The default type will be used if the type can't be determined from inspecting87// the metadata. This is particularly useful when differentiating between web pages88// and blog posts.89export function documentCSL(90input: string,91inputMetadata: Metadata,92defaultType: CSLType,93outputFile?: string,94offset?: string,95): { csl: CSL; extras: CSLExtras } {96const citationMetadata = citationMeta(inputMetadata);9798// The type99const type = citationMetadata[kType]100? cslType(citationMetadata[kType] as string)101: defaultType;102103// The title104const title = (citationMetadata[kTitle] || inputMetadata[kTitle]) as string;105const csl: CSL = {106title,107type,108};109110// The citation key111const key = citationMetadata[kCitationKey] as string | undefined;112if (key) {113csl[kCitationKey] = key;114}115116// Author117const authors = parseAuthor(118citationMetadata[kAuthor] || inputMetadata[kAuthor] ||119inputMetadata[kAuthors],120);121csl.author = cslNames(122authors?.filter((auth) => auth !== undefined).map((auth) => auth?.name),123);124125// Editors126const editors = parseAuthor(citationMetadata[kEditor]);127csl.editor = cslNames(128editors?.filter((editor) => editor !== undefined).map((editor) =>129editor?.name130),131);132if (csl.editor && csl.editor.length === 0) {133delete csl.editor;134}135136// Categories137const categories = citationMetadata[kCategories] ||138inputMetadata[kCategories];139if (categories) {140csl.categories = Array.isArray(categories) ? categories : [categories];141}142143// Language144const language = (citationMetadata[kLanguage] || inputMetadata[kLang]) as145| string146| undefined;147if (language) {148csl.language = language;149}150151// Date152const availableDate = citationMetadata[kAvailableDate] ||153resolveAndFormatDate(input, inputMetadata[kDate]);154if (availableDate) {155csl[kAvailableDate] = cslDate(availableDate);156}157158// Issued date159const issued = citationMetadata[kIssued] ||160resolveAndFormatDate(input, inputMetadata[kDate]);161if (issued) {162csl.issued = cslDate(issued);163}164165// The abstract166const abstract = citationMetadata[kAbstract] || inputMetadata[kAbstract];167if (abstract) {168csl.abstract = abstract as string;169}170171// The publisher172const publisher = citationMetadata[kPublisher];173if (publisher) {174csl.publisher = publisher as string;175}176177// The publication178const containerTitle = citationMetadata[kContainerTitle];179if (containerTitle) {180csl[kContainerTitle] = containerTitle as string;181}182183// The id for this item184csl.id = citationMetadata[kId] as string ||185suggestId(csl.author, csl.issued);186187// This is a helper function that will search188// metadata for the original key, or a transformed189// version of it (for example, all upper, then all lower)190const findValue = (191baseKey: string,192metadata: Metadata[],193transform: (key: string) => string,194) => {195const keys = [baseKey, transform(baseKey)];196for (const key of keys) {197for (const md of metadata) {198const value = md[key] as199| string200| undefined;201if (value) {202return value;203}204}205}206};207const lowercase = (key: string) => {208return key.toLocaleLowerCase();209};210const kebabcase = (key: string) => {211return key.replaceAll("_", "_");212};213214// Url215const url = findValue(kURL, [citationMetadata], lowercase);216if (url) {217csl.URL = url;218} else {219csl.URL = synthesizeCitationUrl(input, inputMetadata, outputFile, offset);220}221222// The DOI223const doi = findValue(kDOI, [citationMetadata, inputMetadata], lowercase);224if (doi) {225csl.DOI = doi;226}227228const issue = citationMetadata[kIssue];229if (issue) {230csl.issue = issue as string;231}232233const volume = citationMetadata[kVolume];234if (volume) {235csl.volume = volume as string;236}237238const number = citationMetadata[kNumber];239if (number) {240csl.number = number as string;241}242243const isbn = findValue(kISBN, [citationMetadata], lowercase);244if (isbn) {245csl.ISBN = isbn as string;246}247248const issn = findValue(kISSN, [citationMetadata], lowercase);249if (issn) {250csl.ISSN = issn as string;251}252253const pmcid = findValue(kPMCID, [citationMetadata], lowercase);254if (pmcid) {255csl.PMCID = pmcid as string;256}257258const pmid = findValue(kPMID, [citationMetadata], lowercase);259if (pmid) {260csl.PMID = pmid as string;261}262263const pageRange = pages(citationMetadata);264if (pageRange.firstPage) {265csl["page-first"] = pageRange.firstPage;266}267if (pageRange.lastPage) {268csl["page-last"] = pageRange.lastPage;269}270if (pageRange.page) {271csl.page = pageRange.page;272}273274const archiveCollection = findValue(275kArchiveCollection,276[citationMetadata],277kebabcase,278);279if (archiveCollection) {280csl[kArchiveCollection] = archiveCollection;281}282283const archiveLocation = findValue(284kArchiveLocation,285[citationMetadata],286kebabcase,287);288if (archiveLocation) {289csl[kArchiveLocation] = archiveLocation;290}291292const forwardStringValue = (key: string) => {293if (citationMetadata[key] !== undefined) {294csl[key] = citationMetadata[key] as string;295}296};297[298"title-short",299"annote",300"archive",301"archive-place",302"authority",303"call-number",304"chapter-number",305"citation-number",306"citation-label",307"collection-number",308"collection-title",309"container-title-short",310"dimensions",311"division",312"edition",313"event-title",314"event-place",315"first-reference-note-number",316"genre",317"jurisdiction",318"keyword",319"locator",320"medium",321"note",322"number",323"number-of-pages",324"number-of-volumes",325"original-publisher",326"original-publisher-place",327"original-title",328"part",329"part-title",330"printing",331"publisher-place",332"references",333"reviewed-genre",334"reviewed-title",335"scale",336"section",337"source",338"status",339"supplement",340"version",341"volume-title",342"volume-title-short",343"year-suffix",344].forEach(forwardStringValue);345346const forwardCSLNameValue = (key: string) => {347if (citationMetadata[key]) {348const authors = parseAuthor(citationMetadata[key]);349csl[key] = cslNames(350authors?.filter((auth) => auth !== undefined).map((auth) => auth?.name),351);352}353};354[355"chair",356"collection-editor",357"compiler",358"composer",359"container-author",360"contributor",361"curator",362"director",363"editor",364"editorial-director",365"executive-producer",366"guest",367"host",368"interviewer",369"illustrator",370"narrator",371"organizer",372"original-author",373"performer",374"producer",375"recipient",376"reviewed-author",377"script-writer",378"series-creator",379"translator",380].forEach(forwardCSLNameValue);381382const forwardCSLDateValue = (key: string) => {383if (citationMetadata[key]) {384csl[key] = cslDate(citationMetadata[key]);385}386};387["accessed", "event-date", "original-date", "submitted"].forEach(388forwardCSLDateValue,389);390391// Forward custom values392const custom = citationMetadata[kCustom];393if (custom) {394// TODO: Could consider supporting note 'cheater codes' which are the old way of doing this395csl[kCustom] = custom;396}397398// Process anything extra399const extras: CSLExtras = {};400401// Process keywords402const kwString = citationMetadata.keyword;403if (kwString && typeof kwString === "string") {404extras.keywords = kwString.split(",");405} else if (inputMetadata.keywords) {406const kw = inputMetadata.keywords;407extras.keywords = Array.isArray(kw) ? kw : [kw];408}409410// Process extra URLS411if (citationMetadata[kPdfUrl]) {412extras[kPdfUrl] = citationMetadata[kPdfUrl] as string;413}414if (citationMetadata[kAbstractUrl]) {415extras[kAbstractUrl] = citationMetadata[kAbstractUrl] as string;416}417418if (citationMetadata[kEIssn]) {419extras[kEIssn] = citationMetadata[kEIssn] as string;420}421422return {423csl,424extras,425};426}427428interface PageRange {429firstPage?: string;430lastPage?: string;431page?: string;432}433434export function citationMeta(metadata: Metadata): Metadata {435if (typeof (metadata[kCitation]) === "object") {436return metadata[kCitation] as Record<string, unknown>;437} else {438return {} as Record<string, unknown>;439}440}441442export function synthesizeCitationUrl(443input: string,444metadata: Metadata,445outputFile?: string,446offset?: string,447) {448const siteMeta = metadata[kWebsite] as Metadata | undefined;449let baseUrl = siteMeta?.[kSiteUrl] as string;450451if (baseUrl && outputFile && offset) {452baseUrl = baseUrl.replace(/\/$/, "");453const rootDir = normalizePath(join(dirname(input), offset));454if (outputFile === "index.html") {455const part = pathWithForwardSlashes(relative(rootDir, dirname(input)));456if (part.length === 0) {457return `${baseUrl}/`;458} else {459return `${baseUrl}/${part}/`;460}461} else {462const relativePath = relative(463rootDir,464join(dirname(input), basename(outputFile)),465);466const part = pathWithForwardSlashes(relativePath);467return `${baseUrl}/${part}`;468}469} else {470// The url is unknown471return undefined;472}473}474475function pages(citationMetadata: Metadata): PageRange {476let firstPage = citationMetadata[kFirstPage];477let lastPage = citationMetadata[kLastPage];478let pages = citationMetadata[kPage]479? `${citationMetadata[kPage] as string}` // Force pages to string in case user writes `page: 7`480: undefined;481if (pages && pages.includes("-")) {482const pagesSplit = pages.split("-");483if (!firstPage) {484firstPage = pagesSplit[0];485}486487if (!lastPage) {488lastPage = pagesSplit[1];489}490} else if (pages && !firstPage) {491firstPage = pages;492} else if (!pages) {493if (firstPage && lastPage) {494pages = `${firstPage} - ${lastPage}`;495} else if (firstPage) {496pages = `${firstPage}`;497}498}499return {500firstPage: firstPage as string,501lastPage: lastPage as string,502page: pages,503};504}505506507