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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 774 additions and 18 deletions

View file

@ -25,6 +25,8 @@ This is useful if you `docker push` your images to Docker Hub. It provides an ea
| `repository` | Docker Hub repository in the format `<namespace>/<name>`. | `github.repository` | | `repository` | Docker Hub repository in the format `<namespace>/<name>`. | `github.repository` |
| `short-description` | Docker Hub repository short description. | | | `short-description` | Docker Hub repository short description. | |
| `readme-filepath` | Path to the repository readme. | `./README.md` | | `readme-filepath` | Path to the repository readme. | `./README.md` |
| `enable-url-completion` | Enables completion of relative URLs to absolute ones. See also [Known Issues](#known-issues). | `false` |
| `image-extensions` | File extensions that will be treated as images. | `bmp,gif,jpg,jpeg,png,svg,webp` |
#### Content limits #### Content limits
@ -86,6 +88,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_PASSWORD }} password: ${{ secrets.DOCKERHUB_PASSWORD }}
repository: peterevans/dockerhub-description repository: peterevans/dockerhub-description
short-description: ${{ github.event.repository.description }} short-description: ${{ github.event.repository.description }}
enable-url-completion: true
``` ```
Updates the Docker Hub repository description whenever a new release is created. Updates the Docker Hub repository description whenever a new release is created.
@ -122,6 +125,38 @@ docker run -v $PWD:/workspace \
peterevans/dockerhub-description:3 peterevans/dockerhub-description:3
``` ```
## Known Issues
The completion of relative urls has some known issues:
1. Relative markdown links in inline-code and code blocks **are also converted**:
```markdown
[link in inline code](#table-of-content)
```
will be converted into
```markdown
[link in inline code](https://github.com/peter-evans/dockerhub-description/blob/main/./README.md#table-of-content)
```
2. Links containing square brackets (`]`) in the text fragment **are not converted**:
```markdown
[[link text with square brackets]](#table-of-content)
```
3. [Reference-style links/images](https://www.markdownguide.org/basic-syntax/#reference-style-links) **are not converted**.
```markdown
[table-of-content][toc]
...
[toc]: #table-of-content "Table of content"
```
## License ## License
[MIT](LICENSE) [MIT](LICENSE)

View file

@ -0,0 +1,335 @@
import {completeRelativeUrls} from '../src/readme-helper'
describe('complete relative urls tests', () => {
const GITHUB_SERVER_URL = process.env['GITHUB_SERVER_URL']
const GITHUB_REPOSITORY = process.env['GITHUB_REPOSITORY']
const GITHUB_REF_NAME = process.env['GITHUB_REF_NAME']
const README_FILEPATH = './README.md'
const EXPECTED_REPOSITORY_URL = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}`
const EXPECTED_BLOB_URL = `${EXPECTED_REPOSITORY_URL}/blob/${GITHUB_REF_NAME}`
const EXPECTED_RAW_URL = `${EXPECTED_REPOSITORY_URL}/raw/${GITHUB_REF_NAME}`
// known issues
test('reference-style links/image sources are not converted', async () => {
const content = [
'table-of-content][toc]',
'',
'[toc]: #table-of-content "Table of content"'
].join('\n')
expect(completeRelativeUrls(content, README_FILEPATH, true, '')).toEqual(
content
)
})
test('links containing square brackets in the text fragment are not converted', async () => {
expect(
completeRelativeUrls(
'[[text with square brackets]](README.md)',
README_FILEPATH,
true,
''
)
).toEqual('[[text with square brackets]](README.md)')
})
test('links containing square brackets in the text fragment are not converted', async () => {
expect(
completeRelativeUrls('`[text](README.md)`', README_FILEPATH, true, '')
).toEqual(`\`[text](${EXPECTED_BLOB_URL}/README.md)\``)
})
// misc
test('do not change content when disabled', async () => {
expect(
completeRelativeUrls('[text](README.md)', README_FILEPATH, false, '')
).toEqual('[text](README.md)')
})
test('do not change link with mailto protocol', async () => {
expect(
completeRelativeUrls(
'[text](mailto:mail@example.com)',
README_FILEPATH,
true,
''
)
).toEqual(`[text](mailto:mail@example.com)`)
})
test('do not change link with ftp protocol', async () => {
expect(
completeRelativeUrls(
'[text](ftp://example.com)',
README_FILEPATH,
true,
''
)
).toEqual(`[text](ftp://example.com)`)
})
test('do not change link with http protocol', async () => {
expect(
completeRelativeUrls(
'[text](http://example.com)',
README_FILEPATH,
true,
''
)
).toEqual(`[text](http://example.com)`)
})
test('do not change link with https protocol', async () => {
expect(
completeRelativeUrls(
'[text](https://example.com)',
README_FILEPATH,
true,
''
)
).toEqual(`[text](https://example.com)`)
})
test('do not change link with protocol-like beginning', async () => {
expect(
completeRelativeUrls(
'[text](abc://example.com)',
README_FILEPATH,
true,
''
)
).toEqual(`[text](abc://example.com)`)
})
test('do not change image from absolute source with absolute link', async () => {
expect(
completeRelativeUrls(
'[![alttext](https://example.com/image.svg)](https://example.com/image.svg)',
README_FILEPATH,
true,
'svg'
)
).toEqual(
`[![alttext](https://example.com/image.svg)](https://example.com/image.svg)`
)
})
// anchors
test('anchor referencing the current document', async () => {
expect(
completeRelativeUrls(
'[text](#relative-anchor)',
README_FILEPATH,
true,
''
)
).toEqual(`[text](${EXPECTED_BLOB_URL}/README.md#relative-anchor)`)
})
test('anchor referencing the current document with a title', async () => {
expect(
completeRelativeUrls(
'[text](#relative-anchor "the anchor (a title)")',
README_FILEPATH,
true,
''
)
).toEqual(
`[text](${EXPECTED_BLOB_URL}/README.md#relative-anchor "the anchor (a title)")`
)
})
test('anchor referencing the current document with a title and unicode', async () => {
expect(
completeRelativeUrls(
'[text with 🌬](#relative-anchor "the anchor (a title with 🌬)")',
README_FILEPATH,
true,
''
)
).toEqual(
`[text with 🌬](${EXPECTED_BLOB_URL}/README.md#relative-anchor "the anchor (a title with 🌬)")`
)
})
test('anchor referencing another document', async () => {
expect(
completeRelativeUrls(
'[text](OTHER.md#absolute-anchor)',
README_FILEPATH,
true,
''
)
).toEqual(`[text](${EXPECTED_BLOB_URL}/OTHER.md#absolute-anchor)`)
})
test('anchor referencing another document with a title', async () => {
expect(
completeRelativeUrls(
'[text](OTHER.md#absolute-anchor "the anchor (a title)")',
README_FILEPATH,
true,
''
)
).toEqual(
`[text](${EXPECTED_BLOB_URL}/OTHER.md#absolute-anchor "the anchor (a title)")`
)
})
test('anchor with image referencing the current document', async () => {
expect(
completeRelativeUrls(
'[![alttext](image.svg)](#absolute-anchor "the anchor (a title)")',
README_FILEPATH,
true,
'svg'
)
).toEqual(
`[![alttext](${EXPECTED_RAW_URL}/image.svg)](${EXPECTED_BLOB_URL}/README.md#absolute-anchor "the anchor (a title)")`
)
})
test('anchor with image referencing another document', async () => {
expect(
completeRelativeUrls(
'[![alttext](image.svg)](OTHER.md#absolute-anchor "the anchor (a title)")',
README_FILEPATH,
true,
'svg'
)
).toEqual(
`[![alttext](${EXPECTED_RAW_URL}/image.svg)](${EXPECTED_BLOB_URL}/OTHER.md#absolute-anchor "the anchor (a title)")`
)
})
// documents
test('text document', async () => {
expect(
completeRelativeUrls('[text](document.yaml)', README_FILEPATH, true, '')
).toEqual(`[text](${EXPECTED_BLOB_URL}/document.yaml)`)
})
test('pdf document', async () => {
expect(
completeRelativeUrls('[text](document.pdf)', README_FILEPATH, true, '')
).toEqual(`[text](${EXPECTED_BLOB_URL}/document.pdf)`)
})
test('document with a title', async () => {
expect(
completeRelativeUrls(
'[text](document.pdf "the document (a title)")',
README_FILEPATH,
true,
''
)
).toEqual(
`[text](${EXPECTED_BLOB_URL}/document.pdf "the document (a title)")`
)
})
test('document with a title and unicode', async () => {
expect(
completeRelativeUrls(
'[text with 🌬](document.pdf "the document (a title with 🌬)")',
README_FILEPATH,
true,
''
)
).toEqual(
`[text with 🌬](${EXPECTED_BLOB_URL}/document.pdf "the document (a title with 🌬)")`
)
})
// images
test('image with supported file extension', async () => {
expect(
completeRelativeUrls(
'![alttext](image.svg)',
README_FILEPATH,
true,
'svg'
)
).toEqual(`![alttext](${EXPECTED_RAW_URL}/image.svg)`)
})
test('image with unsupported file extension', async () => {
expect(
completeRelativeUrls(
'![alttext](image.svg)',
README_FILEPATH,
true,
'jpeg'
)
).toEqual(`![alttext](${EXPECTED_BLOB_URL}/image.svg)`)
})
test('image without alternate text', async () => {
expect(
completeRelativeUrls('![](image.svg)', README_FILEPATH, true, 'svg')
).toEqual(`![](${EXPECTED_RAW_URL}/image.svg)`)
})
test('image with a title', async () => {
expect(
completeRelativeUrls(
'![alttext](image.svg "the image (a title)")',
README_FILEPATH,
true,
'svg'
)
).toEqual(`![alttext](${EXPECTED_RAW_URL}/image.svg "the image (a title)")`)
})
test('image with relative link', async () => {
expect(
completeRelativeUrls(
'[![alttext](image.svg)](image.svg)',
README_FILEPATH,
true,
'svg'
)
).toEqual(
`[![alttext](${EXPECTED_RAW_URL}/image.svg)](${EXPECTED_BLOB_URL}/image.svg)`
)
})
test('image with a title, unicode and relative link', async () => {
expect(
completeRelativeUrls(
'[![alttext with 🌬](image.🌬.svg "the image.🌬.svg (a title)")](image.🌬.svg)',
README_FILEPATH,
true,
'svg'
)
).toEqual(
`[![alttext with 🌬](${EXPECTED_RAW_URL}/image.🌬.svg "the image.🌬.svg (a title)")](${EXPECTED_BLOB_URL}/image.🌬.svg)`
)
})
test('image from absolute source with relative link', async () => {
expect(
completeRelativeUrls(
'[![alttext](https://example.com/image.svg)](image.svg)',
README_FILEPATH,
true,
'svg'
)
).toEqual(
`[![alttext](https://example.com/image.svg)](${EXPECTED_BLOB_URL}/image.svg)`
)
})
test('image with absolute link', async () => {
expect(
completeRelativeUrls(
'[![alttext](image.svg)](https://example.com/image.svg)',
README_FILEPATH,
true,
'svg'
)
).toEqual(
`[![alttext](${EXPECTED_RAW_URL}/image.svg)](https://example.com/image.svg)`
)
})
})

View file

@ -18,6 +18,14 @@ inputs:
description: > description: >
Path to the repository readme Path to the repository readme
Default: `./README.md` Default: `./README.md`
enable-url-completion:
description: >
Enables completion of relative urls to absolute ones
Default: `false`
image-extensions:
description: >
Extensions of files that you be treated as images
Default: `bmp,gif,jpg,jpeg,png,svg,webp`
runs: runs:
using: 'node16' using: 'node16'
main: 'dist/index.js' main: 'dist/index.js'

192
dist/index.js vendored
View file

@ -121,14 +121,16 @@ var __importStar = (this && this.__importStar) || function (mod) {
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.validateInputs = exports.getInputs = void 0; exports.validateInputs = exports.getInputs = void 0;
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const README_FILEPATH_DEFAULT = './README.md'; const readmeHelper = __importStar(__nccwpck_require__(3367));
function getInputs() { function getInputs() {
const inputs = { const inputs = {
username: core.getInput('username'), username: core.getInput('username'),
password: core.getInput('password'), password: core.getInput('password'),
repository: core.getInput('repository'), repository: core.getInput('repository'),
shortDescription: core.getInput('short-description'), 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 // Environment variable input alternatives and their aliases
if (!inputs.username && process.env['DOCKERHUB_USERNAME']) { if (!inputs.username && process.env['DOCKERHUB_USERNAME']) {
@ -155,15 +157,28 @@ function getInputs() {
if (!inputs.readmeFilepath && process.env['README_FILEPATH']) { if (!inputs.readmeFilepath && process.env['README_FILEPATH']) {
inputs.readmeFilepath = process.env['README_FILEPATH']; 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 // Set defaults
if (!inputs.readmeFilepath) { 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']) { if (!inputs.repository && process.env['GITHUB_REPOSITORY']) {
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.repository = inputs.repository.toLowerCase();
inputs.imageExtensions = inputs.imageExtensions.toLowerCase();
return inputs; return inputs;
} }
exports.getInputs = getInputs; exports.getInputs = getInputs;
@ -222,7 +237,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
const core = __importStar(__nccwpck_require__(2186)); const core = __importStar(__nccwpck_require__(2186));
const inputHelper = __importStar(__nccwpck_require__(5480)); const inputHelper = __importStar(__nccwpck_require__(5480));
const dockerhubHelper = __importStar(__nccwpck_require__(1812)); const dockerhubHelper = __importStar(__nccwpck_require__(1812));
const fs = __importStar(__nccwpck_require__(7147)); const readmeHelper = __importStar(__nccwpck_require__(3367));
const util_1 = __nccwpck_require__(3837); const util_1 = __nccwpck_require__(3837);
function getErrorMessage(error) { function getErrorMessage(error) {
if (error instanceof Error) if (error instanceof Error)
@ -236,9 +251,9 @@ function run() {
core.debug(`Inputs: ${(0, util_1.inspect)(inputs)}`); core.debug(`Inputs: ${(0, util_1.inspect)(inputs)}`);
inputHelper.validateInputs(inputs); inputHelper.validateInputs(inputs);
// Fetch the readme content // Fetch the readme content
const readmeContent = yield fs.promises.readFile(inputs.readmeFilepath, { core.info('Reading description source file');
encoding: 'utf8' const readmeContent = yield readmeHelper.getReadmeContent(inputs.readmeFilepath, inputs.enableUrlCompletion, inputs.imageExtensions);
}); core.debug(readmeContent);
// Acquire a token for the Docker Hub API // Acquire a token for the Docker Hub API
core.info('Acquiring token'); core.info('Acquiring token');
const token = yield dockerhubHelper.getToken(inputs.username, inputs.password); const token = yield dockerhubHelper.getToken(inputs.username, inputs.password);
@ -256,6 +271,167 @@ function run() {
run(); run();
/***/ }),
/***/ 3367:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.completeRelativeUrls = exports.getReadmeContent = exports.ENABLE_URL_COMPLETION_DEFAULT = exports.IMAGE_EXTENSIONS_DEFAULT = exports.README_FILEPATH_DEFAULT = void 0;
const core = __importStar(__nccwpck_require__(2186));
const fs = __importStar(__nccwpck_require__(7147));
exports.README_FILEPATH_DEFAULT = './README.md';
exports.IMAGE_EXTENSIONS_DEFAULT = 'bmp,gif,jpg,jpeg,png,svg,webp';
exports.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']}/`;
function getReadmeContent(readmeFilepath, enableUrlCompletion, imageExtensions) {
return __awaiter(this, void 0, void 0, function* () {
// Fetch the readme content
let readmeContent = yield fs.promises.readFile(readmeFilepath, {
encoding: 'utf8'
});
readmeContent = completeRelativeUrls(readmeContent, readmeFilepath, enableUrlCompletion, imageExtensions);
return readmeContent;
});
}
exports.getReadmeContent = getReadmeContent;
function completeRelativeUrls(readmeContent, readmeFilepath, enableUrlCompletion, imageExtensions) {
if (enableUrlCompletion) {
readmeFilepath = readmeFilepath.replace(/^[.][/]/, '');
// Make relative urls absolute
const rules = [
...getRelativeReadmeAnchorsRules(readmeFilepath),
...getRelativeImageUrlRules(imageExtensions),
...getRelativeUrlRules()
];
readmeContent = applyRules(rules, readmeContent);
}
return readmeContent;
}
exports.completeRelativeUrls = completeRelativeUrls;
function applyRules(rules, readmeContent) {
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) {
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 = [
// 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) {
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 = [
// 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() {
// 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 = [
// 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;
}
/***/ }), /***/ }),
/***/ 7351: /***/ 7351:

View file

@ -9,3 +9,8 @@ module.exports = {
}, },
verbose: true verbose: true
} }
process.env = Object.assign(process.env, {
GITHUB_SERVER_URL: 'https://github.com',
GITHUB_REPOSITORY: 'peter-evans/dockerhub-description',
GITHUB_REF_NAME: 'main'
})

View file

@ -9,7 +9,7 @@
"format": "prettier --write '**/*.ts'", "format": "prettier --write '**/*.ts'",
"format-check": "prettier --check '**/*.ts'", "format-check": "prettier --check '**/*.ts'",
"lint": "eslint src/**/*.ts", "lint": "eslint src/**/*.ts",
"test": "jest --passWithNoTests" "test": "jest"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -1,6 +1,5 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as readmeHelper from './readme-helper'
const README_FILEPATH_DEFAULT = './README.md'
interface Inputs { interface Inputs {
username: string username: string
@ -8,6 +7,8 @@ interface Inputs {
repository: string repository: string
shortDescription: string shortDescription: string
readmeFilepath: string readmeFilepath: string
enableUrlCompletion: boolean
imageExtensions: string
} }
export function getInputs(): Inputs { export function getInputs(): Inputs {
@ -16,7 +17,9 @@ export function getInputs(): Inputs {
password: core.getInput('password'), password: core.getInput('password'),
repository: core.getInput('repository'), repository: core.getInput('repository'),
shortDescription: core.getInput('short-description'), 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 // Environment variable input alternatives and their aliases
@ -50,16 +53,31 @@ export function getInputs(): Inputs {
inputs.readmeFilepath = process.env['README_FILEPATH'] 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 // Set defaults
if (!inputs.readmeFilepath) { 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']) { if (!inputs.repository && process.env['GITHUB_REPOSITORY']) {
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.repository = inputs.repository.toLowerCase()
inputs.imageExtensions = inputs.imageExtensions.toLowerCase()
return inputs return inputs
} }

View file

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