Skip to content

Commit

Permalink
feat(changelog): add support for changelog blocks (#226)
Browse files Browse the repository at this point in the history
  • Loading branch information
Feverqwe authored Jun 1, 2023
1 parent fe83903 commit 8f5670b
Show file tree
Hide file tree
Showing 8 changed files with 431 additions and 16 deletions.
86 changes: 70 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"chalk": "4.1.2",
"get-root-node-polyfill": "1.0.0",
"github-slugger": "1.4.0",
"js-yaml": "^4.1.0",
"lodash": "4.17.21",
"markdown-it": "13.0.1",
"markdown-it-attrs": "4.1.4",
Expand All @@ -56,6 +57,7 @@
"@types/github-slugger": "1.3.0",
"@types/highlight.js": "10.1.0",
"@types/jest": "28.1.7",
"@types/js-yaml": "^4.0.5",
"@types/lodash": "4.14.183",
"@types/markdown-it": "12.2.3",
"@types/markdown-it-attrs": "4.1.0",
Expand Down
60 changes: 60 additions & 0 deletions src/transform/plugins/changelog/collect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {bold} from 'chalk';
import {ChangelogItem} from './types';
import initMarkdownit from '../../md';
import changelogPlugin from './index';
import imsize from '../imsize';
import {MarkdownItPluginOpts} from '../typings';

const BLOCK_START = '{% changelog %}';
const BLOCK_END = '{% endchangelog %}\n';

function parseChangelogs(str: string, path?: string) {
const {parse, compile, env} = initMarkdownit({
plugins: [changelogPlugin, imsize],
extractChangelogs: true,
path,
});

compile(parse(str));

return env.changelogs || [];
}

type Options = Pick<MarkdownItPluginOpts, 'path' | 'log'> & {
changelogs?: ChangelogItem[];
extractChangelogs?: boolean;
};

const collect = (input: string, {path: filepath, log, changelogs, extractChangelogs}: Options) => {
let result = input;
let lastPos = 0;
const rawChanges = [];

// eslint-disable-next-line no-constant-condition
while (true) {
const pos = result.indexOf(BLOCK_START, lastPos);
lastPos = pos;
if (pos === -1) {
break;
}
const endPos = result.indexOf(BLOCK_END, pos + BLOCK_START.length);
if (endPos === -1) {
log.error(`Changelog block must be closed${filepath ? ` in ${bold(filepath)}` : ''}`);
break;
}

const change = result.slice(pos, endPos + BLOCK_END.length);

rawChanges.push(change);

result = result.slice(0, pos) + result.slice(endPos + BLOCK_END.length);
}

if (rawChanges.length && changelogs && extractChangelogs) {
changelogs.push(...parseChangelogs(rawChanges.join('\n\n'), filepath));
}

return result;
};

export = collect;
165 changes: 165 additions & 0 deletions src/transform/plugins/changelog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import {MarkdownItPluginCb} from '../typings';
import Core from 'markdown-it/lib/parser_core';
import Token from 'markdown-it/lib/token';
import {bold} from 'chalk';
import yaml from 'js-yaml';
import StateCore from 'markdown-it/lib/rules_core/state_core';

interface Metadata {
date: string;
}

interface Options {
extractChangelogs?: boolean;
}

const CHANGELOG_OPEN_RE = /^\{% changelog %}/;
const CHANGELOG_CLOSE_RE = /^\{% endchangelog %}/;

function isOpenToken(tokens: Token[], i: number) {
return (
tokens[i].type === 'paragraph_open' &&
tokens[i + 1].type === 'inline' &&
tokens[i + 2].type === 'paragraph_close' &&
CHANGELOG_OPEN_RE.test(tokens[i + 1].content)
);
}

function isCloseToken(tokens: Token[], i: number) {
return (
tokens[i]?.type === 'paragraph_open' &&
tokens[i + 1].type === 'inline' &&
tokens[i + 2].type === 'paragraph_close' &&
CHANGELOG_CLOSE_RE.test(tokens[i + 1].content)
);
}

function isTitle(tokens: Token[], i = 0) {
return (
tokens[i].type === 'heading_open' &&
tokens[i + 1].type === 'inline' &&
tokens[i + 2].type === 'heading_close'
);
}

function isImageParagraph(tokens: Token[], i = 0) {
return (
tokens[i].type === 'paragraph_open' &&
tokens[i + 1].type === 'inline' &&
tokens[i + 2].type === 'paragraph_close' &&
tokens[i + 1].children?.some((t) => t.type === 'image')
);
}

function parseBody(tokens: Token[], state: StateCore) {
const {md, env} = state;

const metadataToken = tokens.shift();
if (metadataToken?.type !== 'fence') {
throw new Error('Metadata tag not found');
}
const rawMetadata = yaml.load(metadataToken.content) as Metadata;
const metadata = {
...rawMetadata,
date: new Date(rawMetadata.date).toISOString(),
};

if (!isTitle(tokens)) {
throw new Error('Title tag not found');
}
const title = tokens.splice(0, 3)[1].content;

let image;
if (isImageParagraph(tokens)) {
const paragraphTokens = tokens.splice(0, 3);
const imageToken = paragraphTokens[1]?.children?.find((token) => token.type === 'image');
if (imageToken) {
const width = Number(imageToken.attrGet('width'));
const height = Number(imageToken.attrGet('height'));
let ratio;
if (Number.isFinite(width) && Number.isFinite(height)) {
ratio = height / width;
}
let alt = imageToken.attrGet('title') || '';
if (!alt && imageToken.children) {
alt = md.renderer.renderInlineAsText(imageToken.children, md.options, env);
}
image = {
src: imageToken.attrGet('src'),
alt,
ratio,
};
}
}

const description = md.renderer.render(tokens, md.options, env);

return {
...metadata,
title,
image,
description,
};
}

const changelog: MarkdownItPluginCb<Options> = function (md, {extractChangelogs, log, path}) {
const plugin: Core.RuleCore = (state) => {
const {tokens, env} = state;

for (let i = 0, len = tokens.length; i < len; i++) {
const isOpen = isOpenToken(tokens, i);
if (!isOpen) continue;

const openAt = i;
let isCloseFound = false;
while (i < len) {
i++;
if (isCloseToken(tokens, i)) {
isCloseFound = true;
break;
}
}

if (!isCloseFound) {
log.error(`Changelog close tag in not found: ${bold(path)}`);
break;
}

const closeAt = i + 2;

if (env && extractChangelogs) {
const content = tokens.slice(openAt, closeAt + 1);

// cut open
content.splice(0, 3);
// cut close
content.splice(-3);

try {
const change = parseBody(content, state);

if (!env.changelogs) {
env.changelogs = [];
}

env.changelogs.push(change);
} catch (err) {
log.error(`Changelog error: ${(err as Error).message} in ${bold(path)}`);
continue;
}
}

tokens.splice(openAt, closeAt + 1 - openAt);
len = tokens.length;
i = openAt - 1;
}
};

try {
md.core.ruler.before('curly_attributes', 'changelog', plugin);
} catch (e) {
md.core.ruler.push('changelog', plugin);
}
};

export = changelog;
11 changes: 11 additions & 0 deletions src/transform/plugins/changelog/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface ChangelogItem {
title: string;
image: {
src: string;
alt: string;
ratio?: string;
};
description: string;
date: string;
[x: string]: unknown;
}
Loading

0 comments on commit 8f5670b

Please sign in to comment.