Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop

This commit is contained in:
syuilo 2024-01-14 13:00:00 +09:00
commit fc4c868269
14 changed files with 3506 additions and 6 deletions

43
.github/workflows/changelog-check.yml vendored Normal file
View file

@ -0,0 +1,43 @@
name: Check the description in CHANGELOG.md
on:
pull_request:
branches:
- master
- develop
jobs:
check-changelog:
runs-on: ubuntu-latest
steps:
- name: Checkout head
uses: actions/checkout@v4.1.1
- name: Setup Node.js
uses: actions/setup-node@v4.0.1
with:
node-version-file: '.node-version'
- name: Checkout base
run: |
mkdir _base
cp -r .git _base/.git
cd _base
git fetch --depth 1 origin ${{ github.base_ref }}
git checkout origin/${{ github.base_ref }} CHANGELOG.md
- name: Copy to Checker directory for CHANGELOG-base.md
run: cp _base/CHANGELOG.md scripts/changelog-checker/CHANGELOG-base.md
- name: Copy to Checker directory for CHANGELOG-head.md
run: cp CHANGELOG.md scripts/changelog-checker/CHANGELOG-head.md
- name: diff
continue-on-error: true
run: diff -u CHANGELOG-base.md CHANGELOG-head.md
working-directory: scripts/changelog-checker
- name: Setup Checker
run: npm install
working-directory: scripts/changelog-checker
- name: Run Checker
run: npm run run
working-directory: scripts/changelog-checker

View file

@ -21,6 +21,7 @@
### Client
- Feat: 新しいゲームを追加
- Feat: 絵文字の詳細ダイアログを追加
- Feat: 枠線をつけるMFM`$[border.width=1,style=solid,color=fff,radius=0 ...]`を追加
- Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように
- Enhance: チャンネルノートのピン留めをノートのメニューからできるように
- Enhance: 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように

View file

@ -62,6 +62,11 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
return t.match(/^[0-9.]+s$/) ? t : null;
};
const validColor = (c: string | null | undefined): string | null => {
if (c == null) return null;
return c.match(/^[0-9a-f]{3,6}$/i) ? c : null;
};
const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
/**
@ -240,17 +245,30 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
break;
}
case 'fg': {
let color = token.props.args.color;
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
let color = validColor(token.props.args.color);
color = color ?? 'f00';
style = `color: #${color}; overflow-wrap: anywhere;`;
break;
}
case 'bg': {
let color = token.props.args.color;
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
let color = validColor(token.props.args.color);
color = color ?? 'f00';
style = `background-color: #${color}; overflow-wrap: anywhere;`;
break;
}
case 'border': {
let color = validColor(token.props.args.color);
color = color ? `#${color}` : 'var(--accent)';
let b_style = token.props.args.style;
if (
!['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset']
.includes(b_style)
) b_style = 'solid';
const width = parseFloat(token.props.args.width ?? '1');
const radius = parseFloat(token.props.args.radius ?? '0');
style = `border: ${width}px ${b_style} ${color}; border-radius: ${radius}px`;
break;
}
case 'ruby': {
if (token.children.length === 1) {
const child = token.children[0];

View file

@ -108,4 +108,4 @@ export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://xn--931a.moe/assets/error
export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://xn--931a.moe/assets/not-found.jpg';
export const DEFAULT_INFO_IMAGE_URL = 'https://xn--931a.moe/assets/info.jpg';
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];

View file

@ -0,0 +1,9 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: [
'../../packages/shared/.eslintrc.js',
],
};

3
scripts/changelog-checker/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
coverage
.idea

2769
scripts/changelog-checker/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,24 @@
{
"name": "changelog-checker",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
"run": "vite-node src/index.ts",
"test": "vitest run",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"@types/mdast": "4.0.3",
"@types/node": "20.10.7",
"@vitest/coverage-v8": "1.1.3",
"mdast-util-to-string": "4.0.0",
"remark": "15.0.1",
"remark-parse": "11.0.0",
"typescript": "5.3.3",
"unified": "11.0.4",
"vite": "5.0.11",
"vite-node": "1.1.3",
"vitest": "1.1.3"
}
}

View file

@ -0,0 +1,87 @@
import { Release } from './parser.js';
export class Result {
public readonly success: boolean;
public readonly message?: string;
private constructor(success: boolean, message?: string) {
this.success = success;
this.message = message;
}
static ofSuccess(): Result {
return new Result(true);
}
static ofFailed(message?: string): Result {
return new Result(false, message);
}
}
/**
* develop -> masterまたはrelease -> masterを想定したパターン
* base側の先頭とhead側で追加された分のリリースより1つ前のバージョンが等価であるかチェックする
*/
export function checkNewRelease(base: Release[], head: Release[]): Result {
const releaseCountDiff = head.length - base.length;
if (releaseCountDiff <= 0) {
return Result.ofFailed('Invalid release count.');
}
const baseLatest = base[0];
const headPrevious = head[releaseCountDiff];
if (baseLatest.releaseName !== headPrevious.releaseName) {
return Result.ofFailed('Contains unexpected releases.');
}
return Result.ofSuccess();
}
/**
* topic -> developまたはtopic -> masterを想定したパターン
* head側の最新リリース配下に書き加えられているかをチェックする
*/
export function checkNewTopic(base: Release[], head: Release[]): Result {
if (head.length !== base.length) {
return Result.ofFailed('Invalid release count.');
}
const headLatest = head[0];
for (let relIdx = 0; relIdx < base.length; relIdx++) {
const baseItem = base[relIdx];
const headItem = head[relIdx];
if (baseItem.releaseName !== headItem.releaseName) {
// リリースの順番が変わってると成立しないのでエラーにする
return Result.ofFailed(`Release is different. base:${baseItem.releaseName}, head:${headItem.releaseName}`);
}
if (baseItem.categories.length !== headItem.categories.length) {
// カテゴリごと書き加えられたパターン
if (headLatest.releaseName !== headItem.releaseName) {
// 最新リリース以外に追記されていた場合
return Result.ofFailed(`There is an error in the update history. expected additions:${headLatest.releaseName}, actual additions:${headItem.releaseName}`);
}
} else {
// カテゴリ数の変動はないのでリスト項目の数をチェック
for (let catIdx = 0; catIdx < baseItem.categories.length; catIdx++) {
const baseCategory = baseItem.categories[catIdx];
const headCategory = headItem.categories[catIdx];
if (baseCategory.categoryName !== headCategory.categoryName) {
// カテゴリの順番が変わっていると成立しないのでエラーにする
return Result.ofFailed(`Category is different. base:${baseCategory.categoryName}, head:${headCategory.categoryName}`);
}
if (baseCategory.items.length !== headCategory.items.length) {
if (headLatest.releaseName !== headItem.releaseName) {
// 最新リリース以外に追記されていた場合
return Result.ofFailed(`There is an error in the update history. expected additions:${headLatest.releaseName}, actual additions:${headItem.releaseName}`);
}
}
}
}
}
return Result.ofSuccess();
}

View file

@ -0,0 +1,33 @@
import * as process from 'process';
import * as fs from 'fs';
import { parseChangeLog } from './parser.js';
import { checkNewRelease, checkNewTopic } from './checker.js';
function abort(message?: string) {
if (message) {
console.error(message);
}
process.exit(1);
}
function main() {
if (!fs.existsSync('./CHANGELOG-base.md') || !fs.existsSync('./CHANGELOG-head.md')) {
console.error('CHANGELOG-base.md or CHANGELOG-head.md is missing.');
return;
}
const base = parseChangeLog('./CHANGELOG-base.md');
const head = parseChangeLog('./CHANGELOG-head.md');
const result = (base.length < head.length)
? checkNewRelease(base, head)
: checkNewTopic(base, head);
if (!result.success) {
abort(result.message);
return;
}
}
main();

View file

@ -0,0 +1,62 @@
import * as fs from 'node:fs';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import { Heading, List, Node } from 'mdast';
import { toString } from 'mdast-util-to-string';
export class Release {
public readonly releaseName: string;
public readonly categories: ReleaseCategory[];
constructor(releaseName: string, categories: ReleaseCategory[] = []) {
this.releaseName = releaseName;
this.categories = [...categories];
}
}
export class ReleaseCategory {
public readonly categoryName: string;
public readonly items: string[];
constructor(categoryName: string, items: string[] = []) {
this.categoryName = categoryName;
this.items = [...items];
}
}
function isHeading(node: Node): node is Heading {
return node.type === 'heading';
}
function isList(node: Node): node is List {
return node.type === 'list';
}
export function parseChangeLog(path: string): Release[] {
const input = fs.readFileSync(path, { encoding: 'utf8' });
const processor = unified().use(remarkParse);
const releases: Release[] = [];
const root = processor.parse(input);
let release: Release | null = null;
let category: ReleaseCategory | null = null;
for (const it of root.children) {
if (isHeading(it) && it.depth === 2) {
// リリース
release = new Release(toString(it));
releases.push(release);
} else if (isHeading(it) && it.depth === 3 && release) {
// リリース配下のカテゴリ
category = new ReleaseCategory(toString(it));
release.categories.push(category);
} else if (isList(it) && category) {
for (const listItem of it.children) {
// カテゴリ配下のリスト項目
category.items.push(toString(listItem));
}
}
}
return releases;
}

View file

@ -0,0 +1,414 @@
import {expect, suite, test} from "vitest";
import {Release, ReleaseCategory} from "../src/parser";
import {checkNewRelease, checkNewTopic} from "../src/checker";
suite('checkNewRelease', () => {
test('headに新しいリリースがある1', () => {
const base = [new Release('2024.12.0')]
const head = [new Release('2024.12.1'), new Release('2024.12.0')]
const result = checkNewRelease(base, head)
expect(result.success).toBe(true)
})
test('headに新しいリリースがある2', () => {
const base = [new Release('2024.12.0')]
const head = [new Release('2024.12.2'), new Release('2024.12.1'), new Release('2024.12.0')]
const result = checkNewRelease(base, head)
expect(result.success).toBe(true)
})
test('リリースの数が同じ', () => {
const base = [new Release('2024.12.0')]
const head = [new Release('2024.12.0')]
const result = checkNewRelease(base, head)
console.log(result.message)
expect(result.success).toBe(false)
})
test('baseにあるリリースがheadにない', () => {
const base = [new Release('2024.12.0')]
const head = [new Release('2024.12.2'), new Release('2024.12.1')]
const result = checkNewRelease(base, head)
console.log(result.message)
expect(result.success).toBe(false)
})
})
suite('checkNewTopic', () => {
test('追記なし', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
])
]
const result = checkNewTopic(base, head)
expect(result.success).toBe(true)
})
test('最新バージョンにカテゴリを追加したときはエラーにならない', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
])
]
const result = checkNewTopic(base, head)
expect(result.success).toBe(true)
})
test('最新バージョンからカテゴリを削除したときはエラーにならない', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat3',
'feat4',
])
])
]
const result = checkNewTopic(base, head)
expect(result.success).toBe(true)
})
test('最新バージョンに追記したときはエラーにならない', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
'feat3',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const result = checkNewTopic(base, head)
expect(result.success).toBe(true)
})
test('最新バージョンから削除したときはエラーにならない', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const result = checkNewTopic(base, head)
expect(result.success).toBe(true)
})
test('古いバージョンにカテゴリを追加したときはエラーになる', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
new ReleaseCategory('Client', [
'feat1',
'feat2',
]),
])
]
const result = checkNewTopic(base, head)
console.log(result.message)
expect(result.success).toBe(false)
})
test('古いバージョンからカテゴリを削除したときはエラーになる', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
])
]
const result = checkNewTopic(base, head)
console.log(result.message)
expect(result.success).toBe(false)
})
test('古いバージョンに追記したときはエラーになる', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
'feat3',
]),
])
]
const result = checkNewTopic(base, head)
console.log(result.message)
expect(result.success).toBe(false)
})
test('古いバージョンから削除したときはエラーになる', () => {
const base = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
])
]
const head = [
new Release('2024.12.1', [
new ReleaseCategory('Server', [
'feat1',
'feat2',
]),
]),
new Release('2024.12.0', [
new ReleaseCategory('Server', [
'feat1',
]),
])
]
const result = checkNewTopic(base, head)
console.log(result.message)
expect(result.success).toBe(false)
})
})

View file

@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"module": "nodenext",
"moduleResolution": "nodenext",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
"strict": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"experimentalDecorators": true,
"noImplicitReturns": true,
"esModuleInterop": true,
"typeRoots": [
"./node_modules/@types"
],
"lib": [
"esnext"
]
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"test/**/*"
]
}

View file

@ -0,0 +1,6 @@
import {defineConfig} from 'vite';
const config = defineConfig({});
export default config;