Skip to content

Commit

Permalink
feat: convert relative urls to absolute
Browse files Browse the repository at this point in the history
  • Loading branch information
mrtwnklr committed Mar 2, 2023
1 parent bbef094 commit 0b49034
Show file tree
Hide file tree
Showing 6 changed files with 409 additions and 17 deletions.
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
| `short-description` | Docker Hub repository short description. | |
| `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` | Extensions of files that you be treated as images. | `bmp,gif,jpg,jpeg,png,svg,webp` |

#### Content limits

Expand Down Expand Up @@ -86,6 +88,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repository: peterevans/dockerhub-description
short-description: ${{ github.event.repository.description }}
enable-url-completion: true
```

Updates the Docker Hub repository description whenever a new release is created.
Expand Down Expand Up @@ -122,6 +125,38 @@ docker run -v $PWD:/workspace \
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

[MIT](LICENSE)
8 changes: 8 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ inputs:
description: >
Path to the repository readme
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:
using: 'node16'
main: 'dist/index.js'
Expand Down
186 changes: 178 additions & 8 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,16 @@ var __importStar = (this && this.__importStar) || function (mod) {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.validateInputs = exports.getInputs = void 0;
const core = __importStar(__nccwpck_require__(2186));
const README_FILEPATH_DEFAULT = './README.md';
const readmeHelper = __importStar(__nccwpck_require__(3367));
function getInputs() {
const inputs = {
username: core.getInput('username'),
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
if (!inputs.username && process.env['DOCKERHUB_USERNAME']) {
Expand All @@ -155,15 +157,28 @@ function getInputs() {
if (!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
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;
}
exports.getInputs = getInputs;
Expand Down Expand Up @@ -222,7 +237,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
const core = __importStar(__nccwpck_require__(2186));
const inputHelper = __importStar(__nccwpck_require__(5480));
const dockerhubHelper = __importStar(__nccwpck_require__(1812));
const fs = __importStar(__nccwpck_require__(7147));
const readmeHelper = __importStar(__nccwpck_require__(3367));
const util_1 = __nccwpck_require__(3837);
function getErrorMessage(error) {
if (error instanceof Error)
Expand All @@ -236,9 +251,9 @@ function run() {
core.debug(`Inputs: ${(0, util_1.inspect)(inputs)}`);
inputHelper.validateInputs(inputs);
// Fetch the readme content
const readmeContent = yield fs.promises.readFile(inputs.readmeFilepath, {
encoding: 'utf8'
});
core.info('Reading description source file');
const readmeContent = yield readmeHelper.getReadmeContent(inputs.readmeFilepath, inputs.enableUrlCompletion, inputs.imageExtensions);
core.debug(readmeContent);
// Acquire a token for the Docker Hub API
core.info('Acquiring token');
const token = yield dockerhubHelper.getToken(inputs.username, inputs.password);
Expand All @@ -256,6 +271,161 @@ function 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.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'
});
if (enableUrlCompletion) {
// Make relative urls absolute
const rules = [
...getRelativeReadmeAnchorsRules(readmeFilepath),
...getRelativeImageUrlRules(imageExtensions),
...getRelativeUrlRules()
];
readmeContent = applyRules(rules, readmeContent);
}
return readmeContent;
});
}
exports.getReadmeContent = getReadmeContent;
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:
Expand Down
28 changes: 23 additions & 5 deletions src/input-helper.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import * as core from '@actions/core'

const README_FILEPATH_DEFAULT = './README.md'
import * as readmeHelper from './readme-helper'

interface Inputs {
username: string
password: string
repository: string
shortDescription: string
readmeFilepath: string
enableUrlCompletion: boolean
imageExtensions: string
}

export function getInputs(): Inputs {
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
12 changes: 8 additions & 4 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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')
Expand Down
Loading

0 comments on commit 0b49034

Please sign in to comment.