feat: convert relative urls to absolute (#125) (#126)

* feat: convert relative urls to absolute

* test: add tests for conversion of relative urls

Co-authored-by: Marty Winkler <mrtwnklr@users.noreply.github.com>
This commit is contained in:
Peter Evans
2023-03-05 11:13:27 +09:00
committed by GitHub
parent bbef094b30
commit e139363f61
9 changed files with 774 additions and 18 deletions

View File

@@ -1,6 +1,5 @@
import * as core from '@actions/core'
const README_FILEPATH_DEFAULT = './README.md'
import * as readmeHelper from './readme-helper'
interface Inputs {
username: string
@@ -8,6 +7,8 @@ interface Inputs {
repository: string
shortDescription: string
readmeFilepath: string
enableUrlCompletion: boolean
imageExtensions: string
}
export function getInputs(): Inputs {
@@ -16,7 +17,9 @@ export function getInputs(): Inputs {
password: core.getInput('password'),
repository: core.getInput('repository'),
shortDescription: core.getInput('short-description'),
readmeFilepath: core.getInput('readme-filepath')
readmeFilepath: core.getInput('readme-filepath'),
enableUrlCompletion: Boolean(core.getInput('enable-url-completion')),
imageExtensions: core.getInput('image-extensions')
}
// Environment variable input alternatives and their aliases
@@ -50,16 +53,31 @@ export function getInputs(): Inputs {
inputs.readmeFilepath = process.env['README_FILEPATH']
}
if (!inputs.enableUrlCompletion && process.env['ENABLE_URL_COMPLETION']) {
inputs.enableUrlCompletion = Boolean(process.env['ENABLE_URL_COMPLETION'])
}
if (!inputs.imageExtensions && process.env['IMAGE_EXTENSIONS']) {
inputs.imageExtensions = process.env['IMAGE_EXTENSIONS']
}
// Set defaults
if (!inputs.readmeFilepath) {
inputs.readmeFilepath = README_FILEPATH_DEFAULT
inputs.readmeFilepath = readmeHelper.README_FILEPATH_DEFAULT
}
if (!inputs.enableUrlCompletion) {
inputs.enableUrlCompletion = readmeHelper.ENABLE_URL_COMPLETION_DEFAULT
}
if (!inputs.imageExtensions) {
inputs.imageExtensions = readmeHelper.IMAGE_EXTENSIONS_DEFAULT
}
if (!inputs.repository && process.env['GITHUB_REPOSITORY']) {
inputs.repository = process.env['GITHUB_REPOSITORY']
}
// Docker repositories must be all lower case
// Enforce lower case, where needed
inputs.repository = inputs.repository.toLowerCase()
inputs.imageExtensions = inputs.imageExtensions.toLowerCase()
return inputs
}

View File

@@ -1,7 +1,7 @@
import * as core from '@actions/core'
import * as inputHelper from './input-helper'
import * as dockerhubHelper from './dockerhub-helper'
import * as fs from 'fs'
import * as readmeHelper from './readme-helper'
import {inspect} from 'util'
function getErrorMessage(error: unknown) {
@@ -17,9 +17,13 @@ async function run(): Promise<void> {
inputHelper.validateInputs(inputs)
// Fetch the readme content
const readmeContent = await fs.promises.readFile(inputs.readmeFilepath, {
encoding: 'utf8'
})
core.info('Reading description source file')
const readmeContent = await readmeHelper.getReadmeContent(
inputs.readmeFilepath,
inputs.enableUrlCompletion,
inputs.imageExtensions
)
core.debug(readmeContent)
// Acquire a token for the Docker Hub API
core.info('Acquiring token')

175
src/readme-helper.ts Normal file
View File

@@ -0,0 +1,175 @@
import * as core from '@actions/core'
import * as fs from 'fs'
export const README_FILEPATH_DEFAULT = './README.md'
export const IMAGE_EXTENSIONS_DEFAULT = 'bmp,gif,jpg,jpeg,png,svg,webp'
export const ENABLE_URL_COMPLETION_DEFAULT = false
const TITLE_REGEX = `(?: +"[^"]+")?`
const REPOSITORY_URL = `${process.env['GITHUB_SERVER_URL']}/${process.env['GITHUB_REPOSITORY']}`
const BLOB_PREFIX = `${REPOSITORY_URL}/blob/${process.env['GITHUB_REF_NAME']}/`
const RAW_PREFIX = `${REPOSITORY_URL}/raw/${process.env['GITHUB_REF_NAME']}/`
type Rule = {
/**
* all left of the relative url belonging to the markdown image/link
*/
left: RegExp
/**
* relative url
*/
url: RegExp
/**
* part to prefix the relative url with (excluding github repository url)
*/
absUrlPrefix: string
}
export async function getReadmeContent(
readmeFilepath: string,
enableUrlCompletion: boolean,
imageExtensions: string
): Promise<string> {
// Fetch the readme content
let readmeContent = await fs.promises.readFile(readmeFilepath, {
encoding: 'utf8'
})
readmeContent = completeRelativeUrls(
readmeContent,
readmeFilepath,
enableUrlCompletion,
imageExtensions
)
return readmeContent
}
export function completeRelativeUrls(
readmeContent: string,
readmeFilepath: string,
enableUrlCompletion: boolean,
imageExtensions: string
): string {
if (enableUrlCompletion) {
readmeFilepath = readmeFilepath.replace(/^[.][/]/, '')
// Make relative urls absolute
const rules = [
...getRelativeReadmeAnchorsRules(readmeFilepath),
...getRelativeImageUrlRules(imageExtensions),
...getRelativeUrlRules()
]
readmeContent = applyRules(rules, readmeContent)
}
return readmeContent
}
function applyRules(rules: Rule[], readmeContent: string): string {
rules.forEach(rule => {
const combinedRegex = `${rule.left.source}[(]${rule.url.source}[)]`
core.debug(`rule: ${combinedRegex}`)
const replacement = `$<left>(${rule.absUrlPrefix}$<url>)`
core.debug(`replacement: ${replacement}`)
readmeContent = readmeContent.replace(
new RegExp(combinedRegex, 'giu'),
replacement
)
})
return readmeContent
}
// has to be applied first to avoid wrong results
function getRelativeReadmeAnchorsRules(readmeFilepath: string): Rule[] {
const prefix = `${BLOB_PREFIX}${readmeFilepath}`
// matches e.g.:
// #table-of-content
// #table-of-content "the anchor (a title)"
const url = new RegExp(`(?<url>#[^)]+${TITLE_REGEX})`)
const rules: Rule[] = [
// matches e.g.:
// [#table-of-content](#table-of-content)
// [#table-of-content](#table-of-content "the anchor (a title)")
{
left: /(?<left>\[[^\]]+\])/,
url: url,
absUrlPrefix: prefix
},
// matches e.g.:
// [![media/image.svg](media/image.svg)](#table-of-content)
// [![media/image.svg](media/image.svg "title a")](#table-of-content "title b")
{
left: /(?<left>\[!\[[^\]]*\]\([^)]+\)\])/,
url: url,
absUrlPrefix: prefix
}
]
return rules
}
function getRelativeImageUrlRules(imageExtensions: string): Rule[] {
const extensionsRegex = imageExtensions.replace(/,/g, '|')
// matches e.g.:
// media/image.svg
// media/image.svg "with title"
const url = new RegExp(
`(?<url>[^:)]+[.](?:${extensionsRegex})${TITLE_REGEX})`
)
const rules: Rule[] = [
// matches e.g.:
// ![media/image.svg](media/image.svg)
// ![media/image.svg](media/image.svg "with title")
{
left: /(?<left>!\[[^\]]*\])/,
url: url,
absUrlPrefix: RAW_PREFIX
}
]
return rules
}
function getRelativeUrlRules(): Rule[] {
// matches e.g.:
// .releaserc.yaml
// README.md#table-of-content "title b"
// .releaserc.yaml "the .releaserc.yaml file (a title)"
const url = new RegExp(`(?<url>[^:)]+${TITLE_REGEX})`)
const rules: Rule[] = [
// matches e.g.:
// [.releaserc.yaml](.releaserc.yaml)
// [.releaserc.yaml](.releaserc.yaml "the .releaserc.yaml file (a title)")
{
left: /(?<left>\[[^\]]+\])/,
url: url,
absUrlPrefix: BLOB_PREFIX
},
// matches e.g.:
// [![media/image.svg](media/image.svg)](media/image.svg)
// [![media/image.svg](media/image.svg)](README.md#table-of-content "title b")
// [![media/image.svg](media/image.svg "title a")](media/image.svg)
// [![media/image.svg](media/image.svg "title a")](media/image.svg "title b")
// [![media/image.svg](media/image.svg "title a")](README.md#table-of-content "title b")
{
left: new RegExp(
`(?<left>\\[!\\[[^\\]]*\\]\\([^)]+${TITLE_REGEX}\\)\\])`
),
url: url,
absUrlPrefix: BLOB_PREFIX
}
]
return rules
}