Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions package-lock.json

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

5 changes: 5 additions & 0 deletions src/lang/locale/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,11 @@ export default {
'name': 'Add Blank Line After YAML',
'description': 'Adds a blank line after the YAML block if it does not end the current file or it is not already followed by at least 1 blank line',
},
// align-table-columns.ts
'align-table-columns': {
'name': 'Align Table Columns',
'description': 'Aligns the table so that all columns are have same length.',
},
// blockquotify-on-paste.ts
'add-blockquote-indentation-on-paste': {
'name': 'Add Blockquote Indentation on Paste',
Expand Down
9 changes: 7 additions & 2 deletions src/lang/locale/zh-cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,11 @@ export default {
'name': 'YAML 块后空行',
'description': '确保 YAML 块后有空行,除非它在文档的结尾',
},
// align-table-columns.ts
'align-table-columns': {
'name': '对齐表格列',
'description': '对齐表格列(CJK字符宽度为2)',
},
// blockquotify-on-paste.ts
'add-blockquote-indentation-on-paste': {
'name': '添加引用块缩进',
Expand Down Expand Up @@ -754,11 +759,11 @@ export default {
'name': '中日韩语与英语或数字之间的空格',
'description': '确保中日韩文与英文数字之间有一个空格 <a href="https://github.com/sparanoid/chinese-copywriting-guidelines">参考链接</a>',
'english-symbols-punctuation-before': {
'name': 'English Punctuations and Symbols Before CJK',
'name': '中日韩文之前的英文标点',
'description': '被认为是英文的在中日韩文字符之"前"找到的非字母标点符号 <b>注意: "*" 会被认为是英文</b>',
},
'english-symbols-punctuation-after': {
'name': 'English Punctuations and Symbols After CJK',
'name': '中日韩文之后的英文标点',
'description': '被认为是英文的在中日韩文字符之"后"找到的非字母标点符号 <b>注意: "*" 会被认为是英文</b>',
},
},
Expand Down
99 changes: 99 additions & 0 deletions src/rules/align-table-columns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {getAllTablesInText} from '../utils/mdast';
import {MarkdownTableFormatter} from '../utils/tables';
import {Options, RuleType} from '../rules';
import RuleBuilder, {ExampleBuilder, OptionBuilderBase} from './rule-builder';
import dedent from 'ts-dedent';
import {IgnoreTypes} from '../utils/ignore-types';
class AlignTableOptions implements Options {
}

@RuleBuilder.register
export default class AlignTable extends RuleBuilder<AlignTableOptions> {
constructor() {
super({
nameKey: 'rules.align-table-columns.name',
descriptionKey: 'rules.align-table-columns.description',
type: RuleType.SPACING,
ruleIgnoreTypes: [IgnoreTypes.yaml, IgnoreTypes.code, IgnoreTypes.math, IgnoreTypes.inlineMath, IgnoreTypes.wikiLink, IgnoreTypes.link],
});
}
get OptionsClass(): new () => AlignTableOptions {
return AlignTableOptions;
}
apply(text: string, options: AlignTableOptions): string {
const tablePositions = getAllTablesInText(text);
let formatedTable = '';
let fmt: MarkdownTableFormatter;

if (tablePositions.length === 0) {
return text;
}

for (const tablePosition of tablePositions) {
const tableText = text.substring(tablePosition.startIndex, tablePosition.endIndex);
fmt = new MarkdownTableFormatter();
formatedTable = fmt.formatTable(tableText);
text = text.replace(tableText, formatedTable);
}

return text;
}
get exampleBuilders(): ExampleBuilder<AlignTableOptions>[] {
return [
new ExampleBuilder({
description: 'Make columns are aligned properly',
before: dedent`
| Column 1 | Column 2 |
|-------|-------|
| foo1| bar1|
| foo2 | bar2 |
| foo3 | bar3 |
`,
after: dedent`
| Column 1 | Column 2 |
|----------|----------|
| foo1 | bar1 |
| foo2 | bar2 |
| foo3 | bar3 |
`,
}),
new ExampleBuilder({
description: 'Columns align with CJK characters',
before: dedent`
| Column 1 | Column 2 |
|-------|-------|
| foo1| bar1|
| CJK| 你好|
| foo3 | bar3 |
`,
after: dedent`
| Column 1 | Column 2 |
|----------|----------|
| foo1 | bar1 |
| CJK | 你好 |
| foo3 | bar3 |
`,
}),
new ExampleBuilder({
description: 'fill lossing separators',
before: dedent`
| Column 1 | Column 2 |
|-------|-------
| foo1| bar1
| CJK| 你好|
| foo3 | bar3 |
`,
after: dedent`
| Column 1 | Column 2 |
|----------|----------|
| foo1 | bar1 |
| CJK | 你好 |
| foo3 | bar3 |
`,
}),
];
}
get optionBuilders(): OptionBuilderBase<AlignTableOptions>[] {
return [];
}
}
151 changes: 151 additions & 0 deletions src/utils/tables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// based on https://github.com/alanwsmith/markdown_table_formatter

import {getEAW} from 'meaw';

function computeWidth(str: string) {
let width = 0;
const normalized = str.normalize('NFC');

for (const char of normalized) {
switch (getEAW(char)) {
case 'F':
case 'W':
width += 2;
break;
case 'A':
width += 2;
break;
default:
width += 1;
}
}
return width;
}

export class MarkdownTableFormatter {
private cells: string[][];
private columnWidths: number[];
private outputTable: string;
static formatTable: string;


constructor() {
this.cells = [];
this.columnWidths = [];
this.outputTable = '';
}

private getColumnWidths() {
this.columnWidths = [];
for (let row_i = 0, row_l = this.cells.length; row_i < row_l; row_i++) {
for (let col_i = 0, col_l = this.cells[row_i].length; col_i < col_l; col_i++) {
const strLen = computeWidth(this.cells[row_i][col_i]);
if (this.columnWidths[col_i] === undefined || this.columnWidths[col_i] < strLen) {
this.columnWidths[col_i] = strLen;
}
}
}
}

private importTable(table: string) {
const tableRows = table.split('\n');

// Remove leading empty lines
while (tableRows[0].indexOf('|') === -1) {
tableRows.shift();
}

for (let row_i = 0, row_l = tableRows.length; row_i < row_l; row_i++) {
if (tableRows[row_i].indexOf('|') === -1) {
continue;
}

this.cells[row_i] = [];

const rowColumns = tableRows[row_i].split('|');

for (let col_i = 0, col_l = rowColumns.length; col_i < col_l; col_i++) {
this.cells[row_i][col_i] = rowColumns[col_i].trim();

// If it's the separator row, parse down the dashes
if (row_i === 1) {
this.cells[row_i][col_i] = this.cells[row_i][col_i].replace(/-+/g, '-');
}
}
}


// Remove leading and trailing rows if they are empty.
this.getColumnWidths();

if (this.columnWidths[0] === 0) {
for (let row_i = 0, row_l = this.cells.length; row_i < row_l; row_i++) {
this.cells[row_i].shift();
}
}

this.getColumnWidths();

// Check to see if the last item in column widths is empty
if (this.columnWidths[this.columnWidths.length - 1] === 0) {
for (let row_i = 0, row_l = this.cells.length; row_i < row_l; row_i++) {
if (this.cells[row_i].length === this.columnWidths.length) {
this.cells[row_i].pop();
}
}
}

this.getColumnWidths();
}


private addMissingCellColumns() {
for (let row_i = 0, row_l = this.cells.length; row_i < row_l; row_i++) {
for (let col_i = 0, col_l = this.columnWidths.length; col_i < col_l; col_i++) {
if (typeof this.cells[row_i][col_i] === 'undefined') {
this.cells[row_i][col_i] = '';
}
}
}
}

private padCellsForOutput() {
for (let row_i = 0, row_l = this.cells.length; row_i < row_l; row_i++) {
const padChar = (row_i !== 1) ? ' ' : '-';
for (let col_i = 0, col_l = this.cells[row_i].length; col_i < col_l; col_i++) {
const strLen = computeWidth(this.cells[row_i][col_i]);
if (strLen < this.columnWidths[col_i]) {
this.cells[row_i][col_i] += padChar.repeat(this.columnWidths[col_i] - strLen);
}
}
}
}

public formatTable(table: string) {
this.importTable(table);
this.getColumnWidths();
this.addMissingCellColumns();
this.padCellsForOutput();

// Header
this.outputTable = '| ';
this.outputTable += this.cells[0].join(' | ');
this.outputTable += ' |\n';

// Separator
this.outputTable += '|-';
this.outputTable += this.cells[1].join('-|-');
this.outputTable += '-|\n';

for (let row_i = 2, row_l = this.cells.length; row_i < row_l; row_i++) {
this.outputTable += '| ';
this.outputTable += this.cells[row_i].join(' | ');
this.outputTable += ' |\n';
}

// Remove trailing empty lines
this.outputTable = this.outputTable.replace(/\n+$/, '');

return this.outputTable;
}
}