mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-24 23:15:46 -05:00
Merge branch 'v4' of https://github.com/jackyzha0/quartz
This commit is contained in:
commit
598b9a1c33
@ -25,8 +25,9 @@ This part of the configuration concerns anything that can affect the whole site.
|
||||
- `enablePopovers`: whether to enable [[popover previews]] on your site.
|
||||
- `analytics`: what to use for analytics on your site. Values can be
|
||||
- `null`: don't use analytics;
|
||||
- `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or
|
||||
- `{ provider: 'google', tagId: <your-google-tag> }`: use Google Analytics
|
||||
- `{ provider: 'google', tagId: '<your-google-tag>' }`: use Google Analytics;
|
||||
- `{ provider: 'plausible' }` (managed) or `{ provider: 'plausible', host: '<your-plausible-host>' }` (self-hosted): use [Plausible](https://plausible.io/);
|
||||
- `{ provider: 'umami', host: '<your-umami-host>', websiteId: '<your-umami-website-id>' }`: use [Umami](https://umami.is/);
|
||||
- `locale`: used for [[i18n]] and date formatting
|
||||
- `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes.
|
||||
- This should also include the subpath if you are [[hosting]] on GitHub pages without a custom domain. For example, if my repository is `jackyzha0/quartz`, GitHub pages would deploy to `https://jackyzha0.github.io/quartz` and the `baseUrl` would be `jackyzha0.github.io/quartz`
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -17,7 +17,7 @@
|
||||
"chokidar": "^3.5.3",
|
||||
"cli-spinner": "^0.2.10",
|
||||
"d3": "^7.8.5",
|
||||
"esbuild-sass-plugin": "^2.16.0",
|
||||
"esbuild-sass-plugin": "^2.16.1",
|
||||
"flexsearch": "0.7.43",
|
||||
"github-slugger": "^2.0.0",
|
||||
"globby": "^14.0.0",
|
||||
@ -2052,9 +2052,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-sass-plugin": {
|
||||
"version": "2.16.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.16.0.tgz",
|
||||
"integrity": "sha512-mGCe9MxNYvZ+j77Q/QFO+rwUGA36mojDXkOhtVmoyz1zwYbMaNrtVrmXwwYDleS/UMKTNU3kXuiTtPiAD3K+Pw==",
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.16.1.tgz",
|
||||
"integrity": "sha512-mBB2aEF0xk7yo+Q9pSUh8xYED/1O2wbAM6IauGkDrqy6pl9SbJNakLeLGXiNpNujWIudu8TJTZCv2L5AQYRXtA==",
|
||||
"dependencies": {
|
||||
"resolve": "^1.22.6",
|
||||
"sass": "^1.7.3"
|
||||
|
||||
@ -203,8 +203,9 @@ async function partialRebuildFromEntrypoint(
|
||||
const emitterGraph =
|
||||
(await emitter.getDependencyGraph?.(ctx, processedFiles, staticResources)) ?? null
|
||||
|
||||
// emmiter may not define a dependency graph. nothing to update if so
|
||||
if (emitterGraph) {
|
||||
// only update the graph if the emitter plugin uses the changed file
|
||||
// eg. Assets plugin ignores md files, so we skip updating the graph
|
||||
if (emitterGraph?.hasNode(fp)) {
|
||||
// merge the new dependencies into the dep graph
|
||||
dependencies[emitter.name]?.updateIncomingEdgesForNode(emitterGraph, fp)
|
||||
}
|
||||
|
||||
@ -68,13 +68,9 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
||||
folderIndex = new Map()
|
||||
// construct the index for the first time
|
||||
for (const file of allFiles) {
|
||||
if (file.slug?.endsWith("index")) {
|
||||
const folderParts = file.slug?.split("/")
|
||||
// 2nd last to exclude the /index
|
||||
const folderName = folderParts?.at(-2)
|
||||
if (folderName) {
|
||||
folderIndex.set(folderName, file)
|
||||
}
|
||||
const folderParts = file.slug?.split("/")
|
||||
if (folderParts?.at(-1) === "index") {
|
||||
folderIndex.set(folderParts.slice(0, -1).join("/"), file)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -88,7 +84,7 @@ export default ((opts?: Partial<BreadcrumbOptions>) => {
|
||||
let curPathSegment = slugParts[i]
|
||||
|
||||
// Try to resolve frontmatter folder title
|
||||
const currentFile = folderIndex?.get(curPathSegment)
|
||||
const currentFile = folderIndex?.get(slugParts.slice(0, i + 1).join("/"))
|
||||
if (currentFile) {
|
||||
const title = currentFile.frontmatter!.title
|
||||
if (title !== "index") {
|
||||
|
||||
@ -2,6 +2,7 @@ import { formatDate, getDate } from "./Date"
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import readingTime from "reading-time"
|
||||
import { classNames } from "../util/lang"
|
||||
import { i18n } from "../i18n"
|
||||
|
||||
interface ContentMetaOptions {
|
||||
/**
|
||||
@ -30,8 +31,11 @@ export default ((opts?: Partial<ContentMetaOptions>) => {
|
||||
|
||||
// Display reading time if enabled
|
||||
if (options.showReadingTime) {
|
||||
const { text: timeTaken, words: _words } = readingTime(text)
|
||||
segments.push(timeTaken)
|
||||
const { minutes, words: _words } = readingTime(text)
|
||||
const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({
|
||||
minutes: Math.ceil(minutes),
|
||||
})
|
||||
segments.push(displayedTime)
|
||||
}
|
||||
|
||||
return <p class={classNames(displayClass, "content-meta")}>{segments.join(", ")}</p>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { i18n } from "../i18n"
|
||||
import { FullSlug, _stripSlashes, joinSegments, pathToRoot } from "../util/path"
|
||||
import { FullSlug, joinSegments, pathToRoot } from "../util/path"
|
||||
import { JSResourceToScriptElement } from "../util/resources"
|
||||
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import path from "path"
|
||||
|
||||
import style from "../styles/listPage.scss"
|
||||
import { PageList } from "../PageList"
|
||||
import { _stripSlashes, simplifySlug } from "../../util/path"
|
||||
import { stripSlashes, simplifySlug } from "../../util/path"
|
||||
import { Root } from "hast"
|
||||
import { htmlToJsx } from "../../util/jsx"
|
||||
import { i18n } from "../../i18n"
|
||||
@ -24,9 +24,9 @@ export default ((opts?: Partial<FolderContentOptions>) => {
|
||||
|
||||
function FolderContent(props: QuartzComponentProps) {
|
||||
const { tree, fileData, allFiles, cfg } = props
|
||||
const folderSlug = _stripSlashes(simplifySlug(fileData.slug!))
|
||||
const folderSlug = stripSlashes(simplifySlug(fileData.slug!))
|
||||
const allPagesInFolder = allFiles.filter((file) => {
|
||||
const fileSlug = _stripSlashes(simplifySlug(file.slug!))
|
||||
const fileSlug = stripSlashes(simplifySlug(file.slug!))
|
||||
const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug
|
||||
const folderParts = folderSlug.split(path.posix.sep)
|
||||
const fileParts = fileSlug.split(path.posix.sep)
|
||||
|
||||
@ -53,6 +53,14 @@ export default {
|
||||
tableOfContents: {
|
||||
title: "فهرس المحتويات",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) =>
|
||||
minutes == 1
|
||||
? `دقيقة أو أقل للقراءة`
|
||||
: minutes == 2
|
||||
? `دقيقتان للقراءة`
|
||||
: `${minutes} دقائق للقراءة`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
|
||||
@ -53,6 +53,9 @@ export default {
|
||||
tableOfContents: {
|
||||
title: "Inhaltsverzeichnis",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) => `${minutes} min read`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
|
||||
@ -55,6 +55,9 @@ export interface Translation {
|
||||
tableOfContents: {
|
||||
title: string
|
||||
}
|
||||
contentMeta: {
|
||||
readingTime: (variables: { minutes: number }) => string
|
||||
}
|
||||
}
|
||||
pages: {
|
||||
rss: {
|
||||
|
||||
@ -53,6 +53,9 @@ export default {
|
||||
tableOfContents: {
|
||||
title: "Table of Contents",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) => `${minutes} min read`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
|
||||
@ -53,6 +53,9 @@ export default {
|
||||
tableOfContents: {
|
||||
title: "Tabla de Contenidos",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) => `${minutes} min read`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
|
||||
@ -53,6 +53,9 @@ export default {
|
||||
tableOfContents: {
|
||||
title: "Table des Matières",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) => `${minutes} min read`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
|
||||
@ -53,6 +53,9 @@ export default {
|
||||
tableOfContents: {
|
||||
title: "目次",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) => `${minutes} min read`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
|
||||
@ -53,6 +53,9 @@ export default {
|
||||
tableOfContents: {
|
||||
title: "Inhoudsopgave",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) => `${minutes} min read`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
|
||||
@ -53,6 +53,10 @@ export default {
|
||||
tableOfContents: {
|
||||
title: "Cuprins",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) =>
|
||||
minutes == 1 ? `lectură de 1 minut` : `lectură de ${minutes} minute`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
|
||||
@ -53,6 +53,9 @@ export default {
|
||||
tableOfContents: {
|
||||
title: "Зміст",
|
||||
},
|
||||
contentMeta: {
|
||||
readingTime: ({ minutes }) => `${minutes} min read`,
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
rss: {
|
||||
|
||||
@ -4,6 +4,13 @@ import path from "path"
|
||||
import fs from "fs"
|
||||
import { glob } from "../../util/glob"
|
||||
import DepGraph from "../../depgraph"
|
||||
import { Argv } from "../../util/ctx"
|
||||
import { QuartzConfig } from "../../cfg"
|
||||
|
||||
const filesToCopy = async (argv: Argv, cfg: QuartzConfig) => {
|
||||
// glob all non MD files in content folder and copy it over
|
||||
return await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
|
||||
}
|
||||
|
||||
export const Assets: QuartzEmitterPlugin = () => {
|
||||
return {
|
||||
@ -15,7 +22,7 @@ export const Assets: QuartzEmitterPlugin = () => {
|
||||
const { argv, cfg } = ctx
|
||||
const graph = new DepGraph<FilePath>()
|
||||
|
||||
const fps = await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
|
||||
const fps = await filesToCopy(argv, cfg)
|
||||
|
||||
for (const fp of fps) {
|
||||
const ext = path.extname(fp)
|
||||
@ -30,9 +37,8 @@ export const Assets: QuartzEmitterPlugin = () => {
|
||||
return graph
|
||||
},
|
||||
async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
|
||||
// glob all non MD/MDX/HTML files in content folder and copy it over
|
||||
const assetsPath = argv.output
|
||||
const fps = await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
|
||||
const fps = await filesToCopy(argv, cfg)
|
||||
const res: FilePath[] = []
|
||||
for (const fp of fps) {
|
||||
const ext = path.extname(fp)
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
FilePath,
|
||||
FullSlug,
|
||||
SimpleSlug,
|
||||
_stripSlashes,
|
||||
stripSlashes,
|
||||
joinSegments,
|
||||
pathToRoot,
|
||||
simplifySlug,
|
||||
@ -38,7 +38,7 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpt
|
||||
getQuartzComponents() {
|
||||
return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
|
||||
},
|
||||
async getDependencyGraph(ctx, content, _resources) {
|
||||
async getDependencyGraph(_ctx, _content, _resources) {
|
||||
// Example graph:
|
||||
// nested/file.md --> nested/file.html
|
||||
// \-------> nested/index.html
|
||||
@ -75,7 +75,7 @@ export const FolderPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpt
|
||||
)
|
||||
|
||||
for (const [tree, file] of content) {
|
||||
const slug = _stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug
|
||||
const slug = stripSlashes(simplifySlug(file.data.slug!)) as SimpleSlug
|
||||
if (folders.has(slug)) {
|
||||
folderDescriptions[slug] = [tree, file]
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
RelativeURL,
|
||||
SimpleSlug,
|
||||
TransformOptions,
|
||||
_stripSlashes,
|
||||
stripSlashes,
|
||||
simplifySlug,
|
||||
splitAnchor,
|
||||
transformLink,
|
||||
@ -115,7 +115,7 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
|
||||
}
|
||||
|
||||
// need to decodeURIComponent here as WHATWG URL percent-encodes everything
|
||||
const full = decodeURIComponent(_stripSlashes(destCanonical, true)) as FullSlug
|
||||
const full = decodeURIComponent(stripSlashes(destCanonical, true)) as FullSlug
|
||||
const simple = simplifySlug(full)
|
||||
outgoing.add(simple)
|
||||
node.properties["data-slug"] = full
|
||||
|
||||
@ -118,7 +118,10 @@ const calloutLineRegex = new RegExp(/^> *\[\!\w+\][+-]?.*$/, "gm")
|
||||
// #(...) -> capturing group, tag itself must start with #
|
||||
// (?:[-_\p{L}\d\p{Z}])+ -> non-capturing group, non-empty string of (Unicode-aware) alpha-numeric characters and symbols, hyphens and/or underscores
|
||||
// (?:\/[-_\p{L}\d\p{Z}]+)*) -> non-capturing group, matches an arbitrary number of tag strings separated by "/"
|
||||
const tagRegex = new RegExp(/(?:^| )#((?:[-_\p{L}\p{Emoji}\d])+(?:\/[-_\p{L}\p{Emoji}\d]+)*)/, "gu")
|
||||
const tagRegex = new RegExp(
|
||||
/(?:^| )#((?:[-_\p{L}\p{Emoji}\p{M}\d])+(?:\/[-_\p{L}\p{Emoji}\p{M}\d]+)*)/,
|
||||
"gu",
|
||||
)
|
||||
const blockReferenceRegex = new RegExp(/\^([-_A-Za-z0-9]+)$/, "g")
|
||||
const ytLinkRegex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/
|
||||
const videoExtensionRegex = new RegExp(/\.(mp4|webm|ogg|avi|mov|flv|wmv|mkv|mpg|mpeg|3gp|m4v)$/)
|
||||
|
||||
@ -23,22 +23,22 @@ export type FullSlug = SlugLike<"full">
|
||||
export function isFullSlug(s: string): s is FullSlug {
|
||||
const validStart = !(s.startsWith(".") || s.startsWith("/"))
|
||||
const validEnding = !s.endsWith("/")
|
||||
return validStart && validEnding && !_containsForbiddenCharacters(s)
|
||||
return validStart && validEnding && !containsForbiddenCharacters(s)
|
||||
}
|
||||
|
||||
/** Shouldn't be a relative path and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path. */
|
||||
export type SimpleSlug = SlugLike<"simple">
|
||||
export function isSimpleSlug(s: string): s is SimpleSlug {
|
||||
const validStart = !(s.startsWith(".") || (s.length > 1 && s.startsWith("/")))
|
||||
const validEnding = !(s.endsWith("/index") || s === "index")
|
||||
return validStart && !_containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s)
|
||||
const validEnding = !endsWith(s, "index")
|
||||
return validStart && !containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s)
|
||||
}
|
||||
|
||||
/** Can be found on `href`s but can also be constructed for client-side navigation (e.g. search and graph) */
|
||||
export type RelativeURL = SlugLike<"relative">
|
||||
export function isRelativeURL(s: string): s is RelativeURL {
|
||||
const validStart = /^\.{1,2}/.test(s)
|
||||
const validEnding = !(s.endsWith("/index") || s === "index")
|
||||
const validEnding = !endsWith(s, "index")
|
||||
return validStart && validEnding && ![".md", ".html"].includes(_getFileExtension(s) ?? "")
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ function sluggify(s: string): string {
|
||||
}
|
||||
|
||||
export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
||||
fp = _stripSlashes(fp) as FilePath
|
||||
fp = stripSlashes(fp) as FilePath
|
||||
let ext = _getFileExtension(fp)
|
||||
const withoutFileExt = fp.replace(new RegExp(ext + "$"), "")
|
||||
if (excludeExt || [".md", ".html", undefined].includes(ext)) {
|
||||
@ -73,7 +73,7 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
||||
let slug = sluggify(withoutFileExt)
|
||||
|
||||
// treat _index as index
|
||||
if (_endsWith(slug, "_index")) {
|
||||
if (endsWith(slug, "_index")) {
|
||||
slug = slug.replace(/_index$/, "index")
|
||||
}
|
||||
|
||||
@ -81,21 +81,21 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
|
||||
}
|
||||
|
||||
export function simplifySlug(fp: FullSlug): SimpleSlug {
|
||||
const res = _stripSlashes(_trimSuffix(fp, "index"), true)
|
||||
const res = stripSlashes(trimSuffix(fp, "index"), true)
|
||||
return (res.length === 0 ? "/" : res) as SimpleSlug
|
||||
}
|
||||
|
||||
export function transformInternalLink(link: string): RelativeURL {
|
||||
let [fplike, anchor] = splitAnchor(decodeURI(link))
|
||||
|
||||
const folderPath = _isFolderPath(fplike)
|
||||
const folderPath = isFolderPath(fplike)
|
||||
let segments = fplike.split("/").filter((x) => x.length > 0)
|
||||
let prefix = segments.filter(_isRelativeSegment).join("/")
|
||||
let fp = segments.filter((seg) => !_isRelativeSegment(seg) && seg !== "").join("/")
|
||||
let prefix = segments.filter(isRelativeSegment).join("/")
|
||||
let fp = segments.filter((seg) => !isRelativeSegment(seg) && seg !== "").join("/")
|
||||
|
||||
// manually add ext here as we want to not strip 'index' if it has an extension
|
||||
const simpleSlug = simplifySlug(slugifyFilePath(fp as FilePath))
|
||||
const joined = joinSegments(_stripSlashes(prefix), _stripSlashes(simpleSlug))
|
||||
const joined = joinSegments(stripSlashes(prefix), stripSlashes(simpleSlug))
|
||||
const trail = folderPath ? "/" : ""
|
||||
const res = (_addRelativeToStart(joined) + trail + anchor) as RelativeURL
|
||||
return res
|
||||
@ -206,8 +206,8 @@ export function transformLink(src: FullSlug, target: string, opts: TransformOpti
|
||||
if (opts.strategy === "relative") {
|
||||
return targetSlug as RelativeURL
|
||||
} else {
|
||||
const folderTail = _isFolderPath(targetSlug) ? "/" : ""
|
||||
const canonicalSlug = _stripSlashes(targetSlug.slice(".".length))
|
||||
const folderTail = isFolderPath(targetSlug) ? "/" : ""
|
||||
const canonicalSlug = stripSlashes(targetSlug.slice(".".length))
|
||||
let [targetCanonical, targetAnchor] = splitAnchor(canonicalSlug)
|
||||
|
||||
if (opts.strategy === "shortest") {
|
||||
@ -230,28 +230,29 @@ export function transformLink(src: FullSlug, target: string, opts: TransformOpti
|
||||
}
|
||||
}
|
||||
|
||||
function _isFolderPath(fplike: string): boolean {
|
||||
// path helpers
|
||||
function isFolderPath(fplike: string): boolean {
|
||||
return (
|
||||
fplike.endsWith("/") ||
|
||||
_endsWith(fplike, "index") ||
|
||||
_endsWith(fplike, "index.md") ||
|
||||
_endsWith(fplike, "index.html")
|
||||
endsWith(fplike, "index") ||
|
||||
endsWith(fplike, "index.md") ||
|
||||
endsWith(fplike, "index.html")
|
||||
)
|
||||
}
|
||||
|
||||
function _endsWith(s: string, suffix: string): boolean {
|
||||
export function endsWith(s: string, suffix: string): boolean {
|
||||
return s === suffix || s.endsWith("/" + suffix)
|
||||
}
|
||||
|
||||
function _trimSuffix(s: string, suffix: string): string {
|
||||
if (_endsWith(s, suffix)) {
|
||||
function trimSuffix(s: string, suffix: string): string {
|
||||
if (endsWith(s, suffix)) {
|
||||
s = s.slice(0, -suffix.length)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
function _containsForbiddenCharacters(s: string): boolean {
|
||||
return s.includes(" ") || s.includes("#") || s.includes("?")
|
||||
function containsForbiddenCharacters(s: string): boolean {
|
||||
return s.includes(" ") || s.includes("#") || s.includes("?") || s.includes("&")
|
||||
}
|
||||
|
||||
function _hasFileExtension(s: string): boolean {
|
||||
@ -262,11 +263,11 @@ function _getFileExtension(s: string): string | undefined {
|
||||
return s.match(/\.[A-Za-z0-9]+$/)?.[0]
|
||||
}
|
||||
|
||||
function _isRelativeSegment(s: string): boolean {
|
||||
function isRelativeSegment(s: string): boolean {
|
||||
return /^\.{0,2}$/.test(s)
|
||||
}
|
||||
|
||||
export function _stripSlashes(s: string, onlyStripPrefix?: boolean): string {
|
||||
export function stripSlashes(s: string, onlyStripPrefix?: boolean): string {
|
||||
if (s.startsWith("/")) {
|
||||
s = s.substring(1)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user