From ea31d9d0c5b70a96189510e5715cf5e6952bcad7 Mon Sep 17 00:00:00 2001 From: "takumi.ishii" Date: Tue, 7 Dec 2021 01:31:05 +0900 Subject: [PATCH] Fix navigation slippage while a escaped or inline-coded pipe character is in table. (fixed #34) --- src/extension.ts | 125 ++++--- src/markdowntable.ts | 680 ++++++++++++++++++++------------------- src/test/sample/table.md | 28 +- 3 files changed, 424 insertions(+), 409 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 79741d7..cf05bc5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -67,7 +67,7 @@ export function activate(context: vscode.ExtensionContext) { const table_selection = new vscode.Selection( new vscode.Position(startLine, 0), new vscode.Position(endLine, 10000)); - let table_text = doc.getText(table_selection); + const table_text = doc.getText(table_selection); // テーブルの変形処理クラス @@ -75,7 +75,6 @@ export function activate(context: vscode.ExtensionContext) { // 元のカーソル位置を取得 const [prevline, prevcharacter] = [cur_selection.active.line - startLine, cur_selection.active.character]; - const [prevRow, prevColumn] = mdt.getCellAtPosition(table_text, prevline, prevcharacter); // テーブルをTableDataにシリアライズ let tableData = mdt.stringToTableData(table_text); @@ -83,6 +82,9 @@ export function activate(context: vscode.ExtensionContext) { return; } + // 元のカーソル位置のセルを取得 + const [prevRow, prevColumn] = tableData.getCellAtPosition(prevline, prevcharacter, false); + // 次のセルが新しい行になるかどうか const isNextRow = (prevColumn + 1 >= tableData.columns.length); const isInsertNewRow = ( @@ -92,40 +94,25 @@ export function activate(context: vscode.ExtensionContext) { (isNextRow && prevRow >= tableData.cells.length + 1) ); - if (withFormat) { - // 次の行が必要なら追加する - if (isInsertNewRow === true) { - tableData = mdt.insertRow(tableData, tableData.cells.length); - } - // テーブルをフォーマット - table_text = mdt.tableDataToFormatTableStr(tableData); + // 次の行が必要なら追加する + if (isInsertNewRow === true) { + tableData = mdt.insertRow(tableData, tableData.cells.length); } - else { - // 次の行が必要なら追加する - if (isInsertNewRow === true) { - table_text += '\n' + tableData.indent + '|' + ' |'.repeat(tableData.columns.length); - } - // | が足りていないときは追加する - if (currentLineText.split('|').length < tableData.columns.length + 2) { - let table_text_lines = table_text.split(/\r\n|\n|\r/); - const cursorRow = cur_selection.active.line - startLine; - table_text_lines[cursorRow] += '|'; - table_text = table_text_lines.join('\r\n'); - } - } + // テーブルをフォーマットしたテキストを取得 + let formatted_text = withFormat ? tableData.toFormatTableStr() : tableData.toString(); //エディタ選択範囲にテキストを反映 editor.edit(edit => { - edit.replace(table_selection, table_text); + edit.replace(table_selection, formatted_text); }); // 新しいカーソル位置を計算 // character の +1 は表セル内の|とデータの間の半角スペース分 const newColumn = (isNextRow === true) ? 0 : prevColumn + 1; - const newRow = (isNextRow === true) ? prevRow + 1 : prevRow; - const [newline, newcharacter] = mdt.getPositionOfCell(table_text, newRow, newColumn); + const newRow = (isNextRow === true) ? prevRow + 1 : prevRow; + const [newline, newcharacter] = tableData.getPositionOfCell(newRow, newColumn, withFormat); const newPosition = new vscode.Position( table_selection.start.line + newline, table_selection.start.character + newcharacter + 1); @@ -190,12 +177,6 @@ export function activate(context: vscode.ExtensionContext) { // 元のカーソル位置を取得 const [prevline, prevcharacter] = [cur_selection.active.line - startLine, cur_selection.active.character]; - const [prevRow, prevColumn] = mdt.getCellAtPosition(table_text, prevline, prevcharacter); - - // 先頭セルだったら何もしない - if (prevColumn <= 0 && prevRow <= 0) { - return; - } // テーブルをTableDataにシリアライズ let tableData = mdt.stringToTableData(table_text); @@ -203,26 +184,31 @@ export function activate(context: vscode.ExtensionContext) { return; } - if (withFormat) { - // テーブルをフォーマット - table_text = mdt.tableDataToFormatTableStr(tableData); - //エディタ選択範囲にテキストを反映 - editor.edit(edit => { - edit.replace(table_selection, table_text); - }); + // 元のカーソル位置のセルを取得 + const [prevRow, prevColumn] = tableData.getCellAtPosition(prevline, prevcharacter, false); + // 先頭セルだったら何もしない + if (prevColumn <= 0 && prevRow <= 0) { + return; } + // テーブルをフォーマットしたテキストを取得 + let formatted_text = withFormat ? tableData.toFormatTableStr() : tableData.toString(); + + //エディタ選択範囲にテキストを反映 + editor.edit(edit => { + edit.replace(table_selection, formatted_text); + }); + // 新しいカーソル位置を計算 // character の +1 は表セル内の|とデータの間の半角スペース分 const newColumn = (prevColumn > 0) ? prevColumn - 1 : tableData.columns.length - 1; const newRow = (prevColumn > 0) ? prevRow : prevRow -1; - const [newline, newcharacter] = mdt.getPositionOfCell(table_text, newRow, newColumn); + const [newline, newcharacter] = tableData.getPositionOfCell(newRow, newColumn, withFormat); let newPosition = new vscode.Position( table_selection.start.line + newline, table_selection.start.character + newcharacter); - if( doc.getText(new vscode.Selection( - table_selection.start.line + newline, table_selection.start.character + newcharacter, - table_selection.start.line + newline, table_selection.start.character + newcharacter + 1)) === ' ') { + if( withFormat || + tableData.getCellData(newRow, newColumn).startsWith(' ')) { newPosition = new vscode.Position( table_selection.start.line + newline, table_selection.start.character + newcharacter + 1); @@ -266,8 +252,7 @@ export function activate(context: vscode.ExtensionContext) { const mdt = new markdowntable.MarkdownTable(); const tableData = mdt.tsvToTableData(text); - //const tableStr = mdt.tableDataToTableStr(tableData); - const newTableStr = mdt.tableDataToFormatTableStr(tableData); + const newTableStr = tableData.toFormatTableStr(); //エディタ選択範囲にテキストを反映 editor.edit(edit => { @@ -291,7 +276,7 @@ export function activate(context: vscode.ExtensionContext) { // const lines = text.split(/\r\n|\n|\r/); // 変換のリスト - let format_list = [] as [vscode.Selection, string][]; + let format_list = [] as [vscode.Selection, markdowntable.TableData][]; // テーブルの変形処理クラス const mdt = new markdowntable.MarkdownTable(); @@ -324,10 +309,9 @@ export function activate(context: vscode.ExtensionContext) { // 表をフォーマットする const tableData = mdt.stringToTableData(table_text); - const tableStrFormatted = mdt.tableDataToFormatTableStr(tableData); // 変換内容をリストに保持する - format_list.push([table_selection, tableStrFormatted]); + format_list.push([table_selection, tableData]); preSearchedLine = endLine; } @@ -338,22 +322,21 @@ export function activate(context: vscode.ExtensionContext) { //エディタ選択範囲にテキストを反映 editor.edit(edit => { for (let i = 0; i < format_list.length; i++) { - const [selection, text] = format_list[i] as [vscode.Selection, string]; + const [selection, tableData] = format_list[i] as [vscode.Selection, markdowntable.TableData]; // カーソルを元のセルと同じ位置にするためにカーソル位置を特定しておく if (selection.contains(editor.selection.active)) { // テーブルの変形処理クラス - const mdt = new markdowntable.MarkdownTable(); - const prevText = doc.getText(selection); const [prevline, prevcharacter] = [editor.selection.active.line - selection.start.line, editor.selection.active.character]; - const [prevRow, prevColumn] = mdt.getCellAtPosition(prevText, prevline, prevcharacter); + const [prevRow, prevColumn] = tableData.getCellAtPosition(prevline, prevcharacter, false); // テキストを置換 - edit.replace(selection, text); + const tableStrFormatted = tableData.toFormatTableStr(); + edit.replace(selection, tableStrFormatted); // 新しいカーソル位置を計算 // character の +1 は表セル内の|とデータの間の半角スペース分 - const [newline, newcharacter] = mdt.getPositionOfCell(text, prevRow, prevColumn); + const [newline, newcharacter] = tableData.getPositionOfCell(prevRow, prevColumn, true); const newPosition = new vscode.Position( selection.start.line + newline, selection.start.character + newcharacter + 1); @@ -361,7 +344,7 @@ export function activate(context: vscode.ExtensionContext) { } else { // テキストを置換 - edit.replace(selection, text); + edit.replace(selection, tableData.toFormatTableStr()); } } }); @@ -419,15 +402,18 @@ export function activate(context: vscode.ExtensionContext) { // 元のカーソル位置を取得 const [prevline, prevcharacter] = [cur_selection.active.line - startLine, cur_selection.active.character]; - const [prevRow, prevColumn] = mdt.getCellAtPosition(table_text, prevline, prevcharacter); + + // テーブルをフォーマット + const tableData = mdt.stringToTableData(table_text); + + // 元のカーソル位置のセルを取得 + const [prevRow, prevColumn] = tableData.getCellAtPosition(prevline, prevcharacter, false); // 挿入位置 const insertPosition = isLeft ? prevColumn : prevColumn + 1; - // テーブルをフォーマット - const tableData = mdt.stringToTableData(table_text); const newTableData = mdt.insertColumn(tableData, insertPosition); - const newTableText = mdt.tableDataToFormatTableStr(newTableData); + const newTableText = newTableData.toFormatTableStr(); //エディタ選択範囲にテキストを反映 editor.edit(edit => { @@ -437,7 +423,7 @@ export function activate(context: vscode.ExtensionContext) { // 新しいカーソル位置を計算 // character の +1 は表セル内の|とデータの間の半角スペース分 const newColumn = insertPosition; - const [newline, newcharacter] = mdt.getPositionOfCell(newTableText, prevRow, newColumn); + const [newline, newcharacter] = newTableData.getPositionOfCell(prevRow, newColumn, true); const newPosition = new vscode.Position( table_selection.start.line + newline, table_selection.start.character + newcharacter + 1); @@ -512,18 +498,18 @@ export function activate(context: vscode.ExtensionContext) { // テーブルの変形処理クラス const mdt = new markdowntable.MarkdownTable(); - // 選択セルを取得 - const [startline, startcharacter] = [cur_selection.start.line - startLine, cur_selection.start.character]; - const [startRow, startColumn] = mdt.getCellAtPosition(table_text, startline, startcharacter); - const [endline, endcharacter] = [cur_selection.end.line - startLine, cur_selection.end.character]; - const [endRow, endColumn] = mdt.getCellAtPosition(table_text, endline, endcharacter); - // テーブルをTableDataにシリアライズ let tableData = mdt.stringToTableData(table_text); if (tableData.aligns[0][0] === undefined) { return; } + // 選択セルを取得 + const [startline, startcharacter] = [cur_selection.start.line - startLine, cur_selection.start.character]; + const [startRow, startColumn] = tableData.getCellAtPosition(startline, startcharacter, false); + const [endline, endcharacter] = [cur_selection.end.line - startLine, cur_selection.end.character]; + const [endRow, endColumn] = tableData.getCellAtPosition(endline, endcharacter, false); + // 選択範囲の列のAlignを変更する if (startRow === endRow) { // 選択範囲の開始位置と終了位置が同じ行内の場合 @@ -547,8 +533,8 @@ export function activate(context: vscode.ExtensionContext) { } } - // テーブルをフォーマット - const newTableText = mdt.tableDataToFormatTableStr(tableData); + // テーブルをフォーマットした文字列を取得 + const newTableText = tableData.toFormatTableStr(); //エディタ選択範囲にテキストを反映 editor.edit(edit => { @@ -557,10 +543,11 @@ export function activate(context: vscode.ExtensionContext) { // 元のカーソル選択位置を計算 const [anchorline, anchorcharacter] = [cur_selection.anchor.line - startLine, cur_selection.anchor.character]; - const [anchorRow, anchorColumn] = mdt.getCellAtPosition(table_text, anchorline, anchorcharacter); + // 元のカーソル選択位置のセルを取得 + const [anchorRow, anchorColumn] = tableData.getCellAtPosition(anchorline, anchorcharacter, false); // 新しいカーソル位置をフォーマット後のテキストから計算 - const [newline, newcharacter] = mdt.getPositionOfCell(newTableText, anchorRow, anchorColumn); + const [newline, newcharacter] = tableData.getPositionOfCell(anchorRow, anchorColumn, true); const newPosition = new vscode.Position( table_selection.start.line + newline, table_selection.start.character + newcharacter + 1); diff --git a/src/markdowntable.ts b/src/markdowntable.ts index c91cc82..7aa08fc 100644 --- a/src/markdowntable.ts +++ b/src/markdowntable.ts @@ -1,223 +1,123 @@ import { workspace } from "vscode"; -class TableData { - public aligns : [string, string][] ; - public columns : string[]; - public cells: string [][]; - public leftovers : string[]; - public indent : string; - - constructor(_aligns: [string, string][], _columns : string[], _cells: string [][], _leftovers : string[], _indent : string){ - this.aligns = _aligns; - this.columns = _columns; - this.cells = _cells; - this.leftovers = _leftovers; - this.indent = _indent; +// 行文字列をセルデータ配列に分解する +// datasNumMin に指定したデータ数に満たない行は '' で埋める +function splitline(linestr: string, datasNumMin :number, fillstr :string = '') { + // 先頭と末尾の|を削除 + linestr = linestr.trim(); + if (linestr.startsWith('|')) { + linestr = linestr.slice(1); + } + if (linestr.endsWith('|')) { + linestr = linestr.slice(0, -1); } -}; - -export class MarkdownTable { - - - public stringToTableData(tableText :string) :TableData { - let lines = tableText.split(/\r\n|\n|\r/); - - // 行文字列をセルデータ配列に分解する - // datasNumMin に指定したデータ数に満たない行は '' で埋める - let splitline = (linestr: string, datasNumMin :number, fillstr :string = '') => { - // 先頭と末尾の|を削除 - linestr = linestr.trim(); - if (linestr.startsWith('|')) { - linestr = linestr.slice(1); - } - if (linestr.endsWith('|')) { - linestr = linestr.slice(0, -1); - } - - // |で分割 - let linedatas : string[] = []; - let startindex = 0; - let endindex = 0; - let isEscaping = false; - let isInInlineCode = false; - for (let i = 0; i < linestr.length; ++i) { - const chara = linestr.charAt(i); - if(chara === '\\') { - // \はエスケープ文字 - isEscaping = true; - endindex++; - continue; - } - if(isEscaping) { - // エスケープ文字の次の文字は|かどうか判定しない - isEscaping = false; - endindex++; - continue; - } - - if(chara === '\`') { - // `の間はインラインコード - isInInlineCode = !isInInlineCode; - endindex++; - continue; - } - if(isInInlineCode) { - // インラインコード中は|かどうか判定しない - endindex++; - continue; - } - - if(chara !== '|') { - // | 以外だったら継続 - endindex++; - continue; - } - - // | だったら分割 - let cellstr = linestr.slice(startindex, endindex); - linedatas.push(cellstr); - startindex = i+1; - endindex = i+1; - } - linedatas.push(linestr.slice(startindex)); - - // 最低データ数分を''で埋めておく - let datas : string[] = new Array(datasNumMin).fill(fillstr); - // 行文字列から取得したデータに置き換える - for (let i = 0; i < linedatas.length; i++) { - datas[i] = linedatas[i]; - } - return datas; - }; - - let getIndent = (linestr: string) => { - if (linestr.trim().startsWith('|')) { - let linedatas = linestr.split('|'); - return linedatas[0]; - } - else { - return ''; - } - }; - - // 1行目 - let columns = splitline(lines[0], 0).map((v)=> v.trim()); - let columnNum = columns.length; - let indent = getIndent(lines[0]); - // 2行目の寄せ記号 - let aligns : [string, string][] = new Array(); - let aligndatas = splitline(lines[1], columnNum, '---').map((v)=> v.trim()); - for (let i = 0; i < columnNum; i++) { - let celldata = aligndatas[i]; - aligns[i] = [celldata[0], celldata.slice(-1)]; + // |で分割 + let linedatas : string[] = []; + let startindex = 0; + let endindex = 0; + let isEscaping = false; + let isInInlineCode = false; + for (let i = 0; i < linestr.length; ++i) { + const chara = linestr.charAt(i); + if(chara === '\\') { + // \はエスケープ文字 + isEscaping = true; + endindex++; + continue; } - - // セルの値を取得 - let cells : string [][] = new Array(); - let leftovers : string[] = new Array(); - let cellrow = -1; - for (let row = 2; row < lines.length; row++) { - cellrow++; - - let linedatas = splitline(lines[row], columnNum); - cells[cellrow] = linedatas.slice(0, columnNum).map((v)=> v.trim()); - - // あまりデータを収集する - leftovers[cellrow] = ''; - if (linedatas.length > columnNum) - { - let leftoverdatas = linedatas.slice(columnNum, linedatas.length); - leftovers[cellrow] = leftoverdatas.join('|'); - } + if(isEscaping) { + // エスケープ文字の次の文字は|かどうか判定しない + isEscaping = false; + endindex++; + continue; } - - return new TableData(aligns, columns, cells, leftovers, indent); - } - public tsvToTableData(srcText :string) : TableData { - // 入力データを行ごとに分割する - let lines = srcText.split(/\r\n|\n|\r/); - // カラムデータ - let columns : string[] = new Array(); - let columntexts = lines[0].split('\t'); - // カラム数 - let columnCount = columntexts.length; + if(chara === '\`') { + // `の間はインラインコード + isInInlineCode = !isInInlineCode; + endindex++; + continue; + } + if(isInInlineCode) { + // インラインコード中は|かどうか判定しない + endindex++; + continue; + } - for (let i = 0; i < columnCount; i++) { - columns[i] = columntexts[i].trim(); + if(chara !== '|') { + // | 以外だったら継続 + endindex++; + continue; } - // 入力データから改行とタブで分割した2次元配列を生成する - let cells: string [][] = new Array(); - // カラム数よりもはみ出たデータ - let leftovers : string[] = new Array(); - for (let row = 1; row < lines.length; row++) { - // 各セルの値 - cells[row - 1] = new Array(); - // 行内のデータが足りない場合に備えて空白文字で埋める - for (let column = 0; column < columnCount; column++) { - cells[row - 1][column] = ' '; - } + // | だったら分割 + let cellstr = linestr.slice(startindex, endindex); + linedatas.push(cellstr); + startindex = i+1; + endindex = i+1; + } + linedatas.push(linestr.slice(startindex)); - // 余りデータを初期化 - leftovers[row - 1] = ''; + // 最低データ数分を''で埋めておく + let datas : string[] = new Array(datasNumMin).fill(fillstr); + // 行文字列から取得したデータに置き換える + for (let i = 0; i < linedatas.length; i++) { + datas[i] = linedatas[i]; + } + return datas; +}; - // 行データをタブで分割 - let lineValues = lines[row].split('\t'); - // 実際の値に置き換える - for (let column = 0; column < lineValues.length; column++) { - if(column >= columnCount){ - // カラムヘッダーよりも多い場合ははみ出しデータ配列に保存 - leftovers[row - 1] += '\t' + lineValues[column]; - continue; - } - cells[row - 1][column] = lineValues[column].trim(); - } - } - // 表の寄せ記号 - let aligns : [string, string][] = new Array(); - for (let column = 0; column < columnCount; column++) { - // 全部左寄せ - aligns[column] = [':', '-']; - } +export class TableData { + public readonly aligns : [string, string][] ; + public readonly columns : string[]; + public readonly cells: string [][]; + public readonly leftovers : string[]; + public readonly indent : string; - return new TableData(aligns, columns, cells, leftovers, ''); + constructor(_text :string, _aligns: [string, string][], _columns : string[], _cells: string [][], _leftovers : string[], _indent : string){ + this.aligns = _aligns; + this.columns = _columns; + this.cells = _cells; + this.leftovers = _leftovers; + this.indent = _indent; } - public tableDataToTableStr(data :TableData) : string { + public toString() :string { + //return this.originalText; + let tableString = ""; // カラムヘッダー行の作成 - tableString += data.indent; - for (let i = 0; i < data.columns.length; i++) { - tableString += '| ' + data.columns[i] + ' '; + tableString += this.indent; + for (let i = 0; i < this.columns.length; i++) { + tableString += '|' + this.columns[i]; } tableString += '|\r\n'; // テーブル記号 - tableString += data.indent; - for (let i = 0; i < data.columns.length; i++) { - let [front, end] = data.aligns[i]; + tableString += this.indent; + for (let i = 0; i < this.columns.length; i++) { + let [front, end] = this.aligns[i]; tableString += '| ' + front + '-' + end + ' '; } tableString += '|\r\n'; // テーブル内の各行 - for (let row = 0; row < data.cells.length; row++) { - tableString += data.indent; - for (let i = 0; i < data.cells[row].length; i++) { - tableString += '| ' + data.cells[row][i] + ' '; + for (let row = 0; row < this.cells.length; row++) { + tableString += this.indent; + for (let i = 0; i < this.cells[row].length; i++) { + tableString += '|' + this.cells[row][i]; } tableString += '|'; // 余りデータがある場合はつなげる - if (data.leftovers[row] !== '') { - tableString += data.leftovers[row]; + if (this.leftovers[row] !== '') { + tableString += this.leftovers[row]; } // 次の行がある場合は改行を付ける - if (row+1 < data.cells.length) { + if (row+1 < this.cells.length) { tableString += '\r\n'; } } @@ -225,96 +125,22 @@ export class MarkdownTable { return tableString; } - // 半角文字は1文字、全角文字は2文字として文字数をカウントする - public getLen(str :string) :number { - let length = 0; - for(let i=0; i= 0xD800 && chc <= 0xDBFF) { - // サロゲートペアの時は1文字読み飛ばす - i++; - } - - // if( (chr >= 0x00 && chr <= 0x80) || - // (chr >= 0xa0 && chr <= 0xff) || - // (chr === 0xf8f0) || - // (chr >= 0xff61 && chr <= 0xff9f) || - // (chr >= 0xf8f1 && chr <= 0xf8f3)){ - // //半角文字の場合は1を加算 - // length += 1; - // }else{ - // //それ以外の文字の場合は2を加算 - // length += 2; - // } - } - //結果を返す - return length; - }; - - private doesUse0Space(charCode :number): boolean { - if ((charCode === 0x02DE) || - (charCode >= 0x0300 && charCode <= 0x036F) || - (charCode >= 0x0483 && charCode <= 0x0487) || - (charCode >= 0x0590 && charCode <= 0x05CF) ) { - return true; - } - return false; - } - - private doesUse2Spaces(charCode :number): boolean { - if ((charCode >= 0x2480 && charCode <= 0x24FF) || - (charCode >= 0x2600 && charCode <= 0x27FF) || - (charCode >= 0x2900 && charCode <= 0x2CFF) || - (charCode >= 0x2E00 && charCode <= 0xFF60) || - (charCode >= 0xFFA0) ) { - return true; - } - return false; - } - - private doesUse3Spaces(charCode :number): boolean { - if (charCode >= 0x1F300 && charCode <= 0x1FBFF) { - return true; - } - return false; - } - - public tableDataToFormatTableStr(tableData :TableData) :string { + public toFormatTableStr() :string { let alignData = workspace.getConfiguration('markdowntable').get('alignData'); let alignHeader = workspace.getConfiguration('markdowntable').get('alignColumnHeader'); - let columnNum = tableData.columns.length; + let columnNum = this.columns.length; // 各列の最大文字数を調べる let maxWidths : number[] = new Array(); // コラムヘッダーの各項目の文字数 - for (let i = 0; i < tableData.columns.length; i++) { - let cellLength = this.getLen(tableData.columns[i].trim()); + for (let i = 0; i < this.columns.length; i++) { + let cellLength = this.getLen(this.columns[i].trim()); // 表の寄せ記号行は最短で半角3文字なので、各セル最低でも半角3文字 maxWidths[i] = (3 > cellLength) ? 3 : cellLength; } - for (let row = 0; row < tableData.cells.length; row++) { - let cells = tableData.cells[row]; + for (let row = 0; row < this.cells.length; row++) { + let cells = this.cells[row]; for (let i = 0; i < cells.length; i++) { if (i > columnNum) { break; } let cellLength = this.getLen(cells[i].trim()); @@ -325,10 +151,10 @@ export class MarkdownTable { let formatted : string[] = new Array(); // 列幅をそろえていく - for (let row = 0; row < tableData.cells.length; row++) { + for (let row = 0; row < this.cells.length; row++) { formatted[row] = ''; - formatted[row] += tableData.indent; - let cells = tableData.cells[row]; + formatted[row] += this.indent; + let cells = this.cells[row]; for (let i = 0; i < columnNum; i++) { let celldata = ''; if (i < cells.length) { @@ -339,7 +165,7 @@ export class MarkdownTable { // | の後にスペースを入れる formatted[row] += '| '; if (alignData) { - let [front, end] = tableData.aligns[i]; + let [front, end] = this.aligns[i]; if (front === ':' && end === ':') { // 中央ぞろえ for(let n = 0; n < (maxWidths[i] - celldata_length) / 2 - 0.5; n++) { @@ -379,26 +205,27 @@ export class MarkdownTable { formatted[row] += '|'; // あまりデータを末尾に着ける - if (tableData.leftovers[row].length > 0) { - formatted[row] += tableData.leftovers[row]; + if (this.leftovers[row].length > 0) { + formatted[row] += this.leftovers[row]; } } // 1行目を成形する let columnHeader = ''; - columnHeader += tableData.indent; + columnHeader += this.indent; for (let i = 0; i < columnNum; i++) { - let columnHeader_length = this.getLen(tableData.columns[i]); + const columnText = this.columns[i].trim(); + const columnHeader_length = this.getLen(columnText); columnHeader += '| '; if (alignHeader) { - let [front, end] = tableData.aligns[i]; + let [front, end] = this.aligns[i]; if (front === ':' && end === ':') { // 中央ぞろえ for(let n = 0; n < (maxWidths[i] - columnHeader_length) / 2 - 0.5; n++) { columnHeader += ' '; } - columnHeader += tableData.columns[i]; + columnHeader += columnText; for(let n = 0; n < (maxWidths[i] - columnHeader_length) / 2; n++) { columnHeader += ' '; } @@ -408,11 +235,11 @@ export class MarkdownTable { for(let n = 0; n < maxWidths[i] - columnHeader_length; n++) { columnHeader += ' '; } - columnHeader += tableData.columns[i]; + columnHeader += columnText; } else { // 左揃え - columnHeader += tableData.columns[i]; + columnHeader += columnText; for(let n = 0; n < maxWidths[i] - columnHeader_length; n++) { columnHeader += ' '; } @@ -420,7 +247,7 @@ export class MarkdownTable { } else { - columnHeader += tableData.columns[i]; + columnHeader += columnText; // 余白を-で埋める for(let n = columnHeader_length; n < maxWidths[i]; n++) { columnHeader += ' '; @@ -433,9 +260,9 @@ export class MarkdownTable { // 2行目を成形する let tablemark = ''; - tablemark += tableData.indent; + tablemark += this.indent; for (let i = 0; i < columnNum; i++) { - let [front, end] = tableData.aligns[i]; + let [front, end] = this.aligns[i]; tablemark += '| ' + front; // 余白を-で埋める @@ -452,77 +279,278 @@ export class MarkdownTable { return formatted.join('\r\n'); } - public insertRow(tableData :TableData, insertAt :number) : TableData { - let columns = tableData.columns; - let aligns = tableData.aligns; - let cells = tableData.cells; - let leftovers = tableData.leftovers; - let column_num = tableData.columns.length; - let indent = tableData.indent; + // 半角文字は1文字、全角文字は2文字として文字数をカウントする + public getLen(str :string) :number { + let length = 0; + for(let i=0; i '')); - leftovers.splice(insertAt, 0, ''); - - return new TableData(aligns, columns, cells, leftovers, indent); - } + let chc = str.charCodeAt(i); + if (chc >= 0xD800 && chc <= 0xDBFF) { + // サロゲートペアの時は1文字読み飛ばす + i++; + } - public insertColumn(tableData :TableData, insertAt :number) : TableData { - let columns = tableData.columns; - let aligns = tableData.aligns; - let cells = tableData.cells; - let leftovers = tableData.leftovers; - let column_num = tableData.columns.length; - let indent = tableData.indent; + // if( (chr >= 0x00 && chr <= 0x80) || + // (chr >= 0xa0 && chr <= 0xff) || + // (chr === 0xf8f0) || + // (chr >= 0xff61 && chr <= 0xff9f) || + // (chr >= 0xf8f1 && chr <= 0xf8f3)){ + // //半角文字の場合は1を加算 + // length += 1; + // }else{ + // //それ以外の文字の場合は2を加算 + // length += 2; + // } + } + //結果を返す + return length; + }; - columns.splice(insertAt, 0, ''); - aligns.splice(insertAt, 0, ['-', '-']); - for (let i = 0; i < cells.length; i++) - { - cells[i].splice(insertAt, 0, ''); + private doesUse0Space(charCode :number): boolean { + if ((charCode === 0x02DE) || + (charCode >= 0x0300 && charCode <= 0x036F) || + (charCode >= 0x0483 && charCode <= 0x0487) || + (charCode >= 0x0590 && charCode <= 0x05CF) ) { + return true; } + return false; + } - return new TableData(aligns, columns, cells, leftovers, indent); + private doesUse2Spaces(charCode :number): boolean { + if ((charCode >= 0x2480 && charCode <= 0x24FF) || + (charCode >= 0x2600 && charCode <= 0x27FF) || + (charCode >= 0x2900 && charCode <= 0x2CFF) || + (charCode >= 0x2E00 && charCode <= 0xFF60) || + (charCode >= 0xFFA0) ) { + return true; + } + return false; } + private doesUse3Spaces(charCode :number): boolean { + if (charCode >= 0x1F300 && charCode <= 0x1FBFF) { + return true; + } + return false; + } + + + // return [line, character] - public getPositionOfCell(tableText :string, cellRow :number, cellColumn :number) : [number, number] { + public getPositionOfCell(cellRow :number, cellColumn :number, isInFormatStr :boolean) : [number, number] { let line = (cellRow <= 0) ? 0 : cellRow; - let character = 0; - let lines = tableText.split(/\r\n|\n|\r/); - let linestr = lines[line]; - let column = -1; - for (let n = 0; n < linestr.length; n++) { - if (linestr[n] === '|') { - column++; - } + let lines = isInFormatStr ? this.toFormatTableStr().split(/\r\n|\n|\r/) : this.toString().split(/\r\n|\n|\r/); + let linestr = lines[cellRow]; - if (column >= cellColumn) { - character = n + 1; - break; - } + let cells = splitline(linestr, this.columns.length); - character = n; + let character = 0; + character += this.indent.length; + character += 1; + for (let i = 0; i < cellColumn; i++) { + character += cells[i].length; + character += 1; } return [line, character]; } // return [row, column] - public getCellAtPosition(tableText :string, line :number, character :number) { + public getCellAtPosition(line :number, character :number, isInFormatStr :boolean) { let row = (line <= 0) ? 0 : line; - let lines = tableText.split(/\r\n|\n|\r/); - let linestr = lines[line]; + let lines = isInFormatStr ? this.toFormatTableStr().split(/\r\n|\n|\r/) : this.toString().split(/\r\n|\n|\r/); + let linestr = lines[row]; + + let cells = splitline(linestr, this.columns.length); let column = -1; + let cell_end = this.indent.length; + for (let cell of cells) { + column ++; + cell_end += 1 + cell.length; - for (let n = 0; n < character; n++) { - if (linestr[n] === '|') { - column++; + if(character <= cell_end) { + break; } } return [row, column]; } + + public getCellData(cellRow :number, cellColumn :number) : string { + if(cellRow === 0) { + return (this.columns.length > cellColumn) ? this.columns[cellColumn] : ""; + } + if(cellRow === 1) { + if(this.aligns.length <= cellColumn) { + return "---"; + } + let [front, end] = this.aligns[cellColumn]; + return ' ' + front + '-' + end + ' '; + } + if(cellRow >= this.cells.length+2) { + return ""; + } + + return this.cells[cellRow-2][cellColumn]; + } +}; + +export class MarkdownTable { + public stringToTableData(tableText :string) :TableData { + let lines = tableText.split(/\r\n|\n|\r/); + + let getIndent = (linestr: string) => { + if (linestr.trim().startsWith('|')) { + let linedatas = linestr.split('|'); + return linedatas[0]; + } + else { + return ''; + } + }; + + // 1行目 + let columns = splitline(lines[0], 0); + let columnNum = columns.length; + let indent = getIndent(lines[0]); + + // 2行目の寄せ記号 + let aligns : [string, string][] = new Array(); + let aligndatas = splitline(lines[1], columnNum, '---').map((v)=> v.trim()); + for (let i = 0; i < columnNum; i++) { + let celldata = aligndatas[i]; + aligns[i] = [celldata[0], celldata.slice(-1)]; + } + + // セルの値を取得 + let cells : string [][] = new Array(); + let leftovers : string[] = new Array(); + let cellrow = -1; + for (let row = 2; row < lines.length; row++) { + cellrow++; + + let linedatas = splitline(lines[row], columnNum); + cells[cellrow] = linedatas.slice(0, columnNum); + + // あまりデータを収集する + leftovers[cellrow] = ''; + if (linedatas.length > columnNum) + { + let leftoverdatas = linedatas.slice(columnNum, linedatas.length); + leftovers[cellrow] = leftoverdatas.join('|'); + } + } + + return new TableData(tableText, aligns, columns, cells, leftovers, indent); + } + + public tsvToTableData(srcText :string) : TableData { + // 入力データを行ごとに分割する + let lines = srcText.split(/\r\n|\n|\r/); + // カラムデータ + let columns : string[] = new Array(); + let columntexts = lines[0].split('\t'); + // カラム数 + let columnCount = columntexts.length; + + for (let i = 0; i < columnCount; i++) { + columns[i] = columntexts[i].trim(); + } + + // 入力データから改行とタブで分割した2次元配列を生成する + let cells: string [][] = new Array(); + // カラム数よりもはみ出たデータ + let leftovers : string[] = new Array(); + for (let row = 1; row < lines.length; row++) { + // 各セルの値 + cells[row - 1] = new Array(); + // 行内のデータが足りない場合に備えて空白文字で埋める + for (let column = 0; column < columnCount; column++) { + cells[row - 1][column] = ' '; + } + + // 余りデータを初期化 + leftovers[row - 1] = ''; + + // 行データをタブで分割 + let lineValues = lines[row].split('\t'); + + // 実際の値に置き換える + for (let column = 0; column < lineValues.length; column++) { + if(column >= columnCount){ + // カラムヘッダーよりも多い場合ははみ出しデータ配列に保存 + leftovers[row - 1] += '\t' + lineValues[column]; + continue; + } + cells[row - 1][column] = lineValues[column].trim(); + } + } + + // 表の寄せ記号 + let aligns : [string, string][] = new Array(); + for (let column = 0; column < columnCount; column++) { + // 全部左寄せ + aligns[column] = [':', '-']; + } + + const table = new TableData("", aligns, columns, cells, leftovers, ''); + return new TableData(table.toFormatTableStr(), aligns, columns, cells, leftovers, ''); + } + + public insertRow(tableData :TableData, insertAt :number) : TableData { + const columns = tableData.columns; + const aligns = tableData.aligns; + const cells = tableData.cells; + const leftovers = tableData.leftovers; + const column_num = tableData.columns.length; + const indent = tableData.indent; + + cells.splice(insertAt, 0, Array.from({length: column_num}, () => ' ')); + leftovers.splice(insertAt, 0, ''); + + const text = tableData.toString() + '\n' + tableData.indent + '|' + ' |'.repeat(tableData.columns.length); + + return new TableData(text, aligns, columns, cells, leftovers, indent); + } + + public insertColumn(tableData :TableData, insertAt :number) : TableData { + let columns = tableData.columns; + let aligns = tableData.aligns; + let cells = tableData.cells; + let leftovers = tableData.leftovers; + let column_num = tableData.columns.length; + let indent = tableData.indent; + + columns.splice(insertAt, 0, ''); + aligns.splice(insertAt, 0, ['-', '-']); + for (let i = 0; i < cells.length; i++) + { + cells[i].splice(insertAt, 0, ''); + } + + const table = new TableData("", aligns, columns, cells, leftovers, indent); + return new TableData(table.toFormatTableStr(), aligns, columns, cells, leftovers, indent); + } } diff --git a/src/test/sample/table.md b/src/test/sample/table.md index bc46a86..ae6dc75 100644 --- a/src/test/sample/table.md +++ b/src/test/sample/table.md @@ -1,19 +1,19 @@ # -| column A | column B \| `|` | | column C | | column D | | -| :------- | :-------------- | :-- | :------- | :-- | ----------: | --- | -| data A1 | data B1 | | data C1 | | \| data D1 | | -| data A2 | data B2 | | data C2 | | `|`data D2 | | -| data A3 | data B3 | | data C3 | | `|` data D3 | | -| data A4 | data B4 | | data C4 | | `data| D4` | | - - -| column A | column B | column C | column D | -| :-- | :--: | --: | :-- | -| data A1 | data B1 | data C1 | data D1 | -| data A2 data A2 | data B2 | data C2 | data D2 | -| data A3 | data B3 data B3 | data C3 | data D3 | -| data A4 | data B4 | data C4 | data D4 | + | column A | column B \| `|` | | column C | | column D | | + | :------- | :-------------- | :-- | :------- | :-- | ----------: | --- | + | data A1 | data B1 | | data C1 | | \| data D1 | | + | data A2 | data B2 | | data C2 | | `|`data D2 | | + | data A3 | data B3 | | data C3 | | `|` data D3 | | + | data A4 | data B4 | | data C4 | | `data| D4` | | + + + | column A | column B | column C | column D | + | :-- | :--: | --: | :-- | + | data A1 | data B1 | data C1 | data D1 | + | data A2 data A2 | data B2 | data C2 | data D2 | + | data A3 | data B3 data B3 | data C3 | data D3 | + | data A4 | data B4 | data C4 | data D4 | | column A | column B | column C | column D |