Initial commit
This commit is contained in:
commit
536ab439aa
|
@ -0,0 +1,9 @@
|
|||
image: scratch
|
||||
|
||||
pages:
|
||||
stage: deploy
|
||||
artifacts:
|
||||
paths:
|
||||
- .
|
||||
only:
|
||||
- master
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2023 blankie
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* line: string, must be trimmed of whitespace beforehand
|
||||
* tableItems: int | null, result output will be padded or trimmed to it if specified
|
||||
*
|
||||
* Returns an array of strings
|
||||
*/
|
||||
function splitByPipe(line, tableItems = null) {
|
||||
let res = [];
|
||||
let item = "";
|
||||
let consecutiveBackslashes = 0;
|
||||
|
||||
if (line.startsWith("|")) {
|
||||
line = line.substring(1);
|
||||
}
|
||||
|
||||
for (let char of line) {
|
||||
if (char === "\\") {
|
||||
item += "\\";
|
||||
consecutiveBackslashes++;
|
||||
} else if (char === "|" && consecutiveBackslashes % 2 === 1) {
|
||||
item = item.substring(0, item.length - 1) + "|";
|
||||
consecutiveBackslashes = 0;
|
||||
} else if (char === "|" && consecutiveBackslashes % 2 === 0) {
|
||||
item = item.trim();
|
||||
res.push(item);
|
||||
|
||||
item = "";
|
||||
consecutiveBackslashes = 0;
|
||||
} else {
|
||||
item += char;
|
||||
consecutiveBackslashes = 0;
|
||||
}
|
||||
}
|
||||
item = item.trim();
|
||||
if (item.length !== 0) {
|
||||
res.push(item);
|
||||
}
|
||||
|
||||
while (tableItems !== null && res.length < tableItems) {
|
||||
res.push("");
|
||||
}
|
||||
while (tableItems !== null && res.length > tableItems) {
|
||||
res.pop();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/*
|
||||
* line: string, must be trimmed of whitespace beforehand
|
||||
* headerRow: array of strings
|
||||
*
|
||||
* Returns an array of objects, which has the following keys:
|
||||
* - text: string
|
||||
* - leftAligned: boolean
|
||||
* - rightAligned: boolean
|
||||
*/
|
||||
function parseDelimiterRow(line, headerRow) {
|
||||
let res = [];
|
||||
let delimiterRow = splitByPipe(line);
|
||||
|
||||
if (delimiterRow.length !== headerRow.length) {
|
||||
throw new Error(`delimiter row is not as long as the header row (${delimiterRow.length} !== ${headerRow.length})`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < headerRow.length; i++) {
|
||||
let header = headerRow[i];
|
||||
let delimiter = delimiterRow[i];
|
||||
let match = delimiter.match(/^(:)?-+(:)?$/);
|
||||
|
||||
if (match === null) {
|
||||
throw new Error(`invalid delimiter row (index: ${i}): ${delimiter}`);
|
||||
}
|
||||
res.push({
|
||||
text: header,
|
||||
leftAligned: match[1] === ":",
|
||||
rightAligned: match[2] === ":"
|
||||
});
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/*
|
||||
* lines: array of strings, each item must be trimmed of whitespace beforehand
|
||||
*
|
||||
* See parseDelimiterRow() for the return type
|
||||
*/
|
||||
function parseHeader(lines) {
|
||||
if (lines.length < 2) {
|
||||
throw new Error(`input must be at least 2 lines long (${lines.length} < 2)`);
|
||||
}
|
||||
|
||||
let headerRow = splitByPipe(lines[0]);
|
||||
if (headerRow.length === 0) {
|
||||
throw new Error("header row must have at least one item");
|
||||
}
|
||||
|
||||
return parseDelimiterRow(lines[1], headerRow);
|
||||
}
|
||||
|
||||
/*
|
||||
* input: string
|
||||
*
|
||||
* Returns an object with the following keys:
|
||||
* - columns: See parseHeader() for the first item
|
||||
* - rows: array of an array of strings
|
||||
*/
|
||||
function deserializeTable(input) {
|
||||
let lines = [];
|
||||
let rows = [];
|
||||
|
||||
for (let line of input.trim().split("\n")) {
|
||||
lines.push(line.trim());
|
||||
}
|
||||
|
||||
let columns = parseHeader(lines);
|
||||
for (let i = 2; i < lines.length; i++) {
|
||||
rows.push(splitByPipe(lines[i], columns.length));
|
||||
}
|
||||
|
||||
return {columns, rows};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export {deserializeTable};
|
|
@ -0,0 +1,73 @@
|
|||
<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Markdown Table Editor</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script asyhc type="module" src="main.mjs"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript><p class="left right">Javascript is required because the closest non-JS alternative is doing everything server-side, and I do not want to bother with that</p></noscript>
|
||||
|
||||
<dialog id="load-dialog">
|
||||
<h3>Load table</h3>
|
||||
<span id="error"></span>
|
||||
<textarea autofocus></textarea>
|
||||
|
||||
<hr>
|
||||
<div class="buttons">
|
||||
<button id="load-button">Load table</button>
|
||||
<form method="dialog">
|
||||
<button>Close</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
<button id="open-load-button">Load table</button>
|
||||
|
||||
<hr>
|
||||
<dialog id="item-context-menu">
|
||||
<form id="alignment-radios">
|
||||
<fieldset>
|
||||
<legend>Column alignment:</legend>
|
||||
<label>
|
||||
<input type="radio" name="alignment" id="none" />
|
||||
None
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="alignment" id="left" />
|
||||
Left
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="alignment" id="center" />
|
||||
Center
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="alignment" id="right" />
|
||||
Right
|
||||
</label>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<br>
|
||||
<button id="delete-row">Delete row</button>
|
||||
<button id="delete-column">Delete column</button>
|
||||
<hr>
|
||||
<button id="insert-row-above">Insert row above</button>
|
||||
<button id="insert-row-below">Insert row below</button>
|
||||
<button id="insert-column-left">Insert column to the left</button>
|
||||
<button id="insert-column-right">Insert column to the right</button>
|
||||
</dialog>
|
||||
|
||||
Hold Shift to suppress table controls
|
||||
|
||||
<div class="table-container">
|
||||
<table></table>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<button id="copy-to-clipboard">Copy to Clipboard</button>
|
||||
<textarea id="output" readonly rows="15"></textarea>
|
||||
|
||||
<hr>
|
||||
<footer class="left right"><a href="https://gitlab.com/blankX/mdtabeditor">Source code</a> (<a href="https://git.nixnet.services/blankie/mdtabeditor">Mirror</a>)</footer>
|
||||
</body>
|
|
@ -0,0 +1,362 @@
|
|||
import {deserializeTable, serializeTable, calculateColumnLength} from "./tables.mjs";
|
||||
|
||||
let loadDialog = document.querySelector("#load-dialog");
|
||||
|
||||
let itemContextMenu = document.querySelector("#item-context-menu");
|
||||
let deleteRow = itemContextMenu.querySelector("#delete-row");
|
||||
let deleteColumn = itemContextMenu.querySelector("#delete-column");
|
||||
let insertRowAbove = itemContextMenu.querySelector("#insert-row-above");
|
||||
let insertRowBelow = itemContextMenu.querySelector("#insert-row-below");
|
||||
let insertColumnLeft = itemContextMenu.querySelector("#insert-column-left");
|
||||
let insertColumnRight = itemContextMenu.querySelector("#insert-column-right");
|
||||
|
||||
let output = document.querySelector("#output");
|
||||
let tableElement = document.querySelector("table");
|
||||
let table = {
|
||||
columns: [
|
||||
{text: "ABC", leftAligned: false, rightAligned: false},
|
||||
{text: "DEF", leftAligned: false, rightAligned: false},
|
||||
{text: "GHI", leftAligned: false, rightAligned: false}
|
||||
],
|
||||
rows: [
|
||||
["1", "2", "3"],
|
||||
["4", "5", "6"],
|
||||
["7", "8", "9"]
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
|
||||
Array.prototype.insert = function(index, item) {
|
||||
this.splice(index, 0, item);
|
||||
};
|
||||
|
||||
Array.prototype.remove = function(index) {
|
||||
this.splice(index, 1);
|
||||
};
|
||||
|
||||
Node.prototype.insertAfter = function(newNode, referenceNode) {
|
||||
this.insertBefore(newNode, referenceNode?.nextSibling);
|
||||
};
|
||||
|
||||
NodeList.prototype.indexOf = function(searchElement) {
|
||||
for (let [index, element] of this.entries()) {
|
||||
if (element === searchElement) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
function findTableIndices(textarea) {
|
||||
let index = tableElement.querySelectorAll("textarea").indexOf(textarea);
|
||||
let columnIndex = index % table.columns.length;
|
||||
let rowIndex = -1;
|
||||
|
||||
if (index >= table.columns.length) {
|
||||
rowIndex = Math.floor(index / table.columns.length - 1);
|
||||
}
|
||||
|
||||
return {columnIndex, rowIndex};
|
||||
}
|
||||
|
||||
function updateItemAlignment(textarea, column) {
|
||||
textarea.classList.remove("left");
|
||||
textarea.classList.remove("right");
|
||||
if (column.leftAligned) {
|
||||
textarea.classList.add("left");
|
||||
}
|
||||
if (column.rightAligned) {
|
||||
textarea.classList.add("right");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function createItemElement(item, column) {
|
||||
let textarea = document.createElement("textarea");
|
||||
textarea.rows = 1;
|
||||
textarea.cols = column.maxLength;
|
||||
textarea.value = item;
|
||||
if (column.leftAligned) {
|
||||
textarea.classList.add("left");
|
||||
}
|
||||
if (column.rightAligned) {
|
||||
textarea.classList.add("right");
|
||||
}
|
||||
return textarea;
|
||||
}
|
||||
|
||||
function createRowElement(row, isHeader) {
|
||||
let tr = document.createElement(isHeader ? "th" : "tr");
|
||||
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
let td = document.createElement("td");
|
||||
td.appendChild(createItemElement(row[i], table.columns[i]));
|
||||
tr.appendChild(td);
|
||||
}
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
function reloadTable() {
|
||||
for (let i = 0; i < table.columns.length; i++) {
|
||||
table.columns[i].maxLength = calculateColumnLength(table.columns[i], table.rows, i);
|
||||
}
|
||||
|
||||
while (tableElement.lastChild) {
|
||||
tableElement.removeChild(tableElement.lastChild);
|
||||
}
|
||||
|
||||
let columnRow = [];
|
||||
for (let column of table.columns) {
|
||||
columnRow.push(column.text);
|
||||
}
|
||||
tableElement.appendChild(createRowElement(columnRow, true));
|
||||
|
||||
for (let row of table.rows) {
|
||||
tableElement.appendChild(createRowElement(row, false));
|
||||
}
|
||||
|
||||
output.value = serializeTable(table);
|
||||
}
|
||||
|
||||
|
||||
|
||||
loadDialog.querySelector("#load-button").addEventListener("click", function() {
|
||||
try {
|
||||
table = deserializeTable(loadDialog.querySelector("textarea").value);
|
||||
reloadTable();
|
||||
loadDialog.close();
|
||||
} catch (e) {
|
||||
loadDialog.querySelector("#error").innerText = e.message;
|
||||
}
|
||||
}, {passive: true});
|
||||
|
||||
document.querySelector("#open-load-button").addEventListener("click", function() {
|
||||
loadDialog.querySelector("textarea").value = "";
|
||||
loadDialog.querySelector("#error").innerText = "";
|
||||
loadDialog.showModal();
|
||||
}, {passive: true});
|
||||
|
||||
|
||||
|
||||
itemContextMenu.querySelector("form").addEventListener("change", function(event) {
|
||||
let leftAligned = ["left", "center"].includes(event.target.id);
|
||||
let rightAligned = ["center", "right"].includes(event.target.id);
|
||||
let columnIndex = parseInt(itemContextMenu.dataset.columnIndex, 10);
|
||||
let column = table.columns[columnIndex];
|
||||
|
||||
column.leftAligned = leftAligned;
|
||||
column.rightAligned = rightAligned;
|
||||
|
||||
updateItemAlignment(tableElement.querySelectorAll("th textarea")[columnIndex], column);
|
||||
for (let tr of tableElement.querySelectorAll("tr")) {
|
||||
updateItemAlignment(tr.querySelectorAll("textarea")[columnIndex], column);
|
||||
}
|
||||
|
||||
output.value = serializeTable(table);
|
||||
});
|
||||
|
||||
deleteRow.addEventListener("click", function() {
|
||||
let rowIndex = parseInt(itemContextMenu.dataset.rowIndex, 10);
|
||||
|
||||
table.rows.remove(rowIndex);
|
||||
tableElement.querySelectorAll("tr")[rowIndex].remove();
|
||||
|
||||
output.value = serializeTable(table);
|
||||
itemContextMenu.close();
|
||||
}, {passive: true});
|
||||
|
||||
deleteColumn.addEventListener("click", function() {
|
||||
let columnIndex = parseInt(itemContextMenu.dataset.columnIndex, 10);
|
||||
|
||||
table.columns.remove(columnIndex);
|
||||
for (let row of table.rows) {
|
||||
row.remove(columnIndex);
|
||||
}
|
||||
|
||||
for (let tr of tableElement.querySelectorAll("th, tr")) {
|
||||
tr.querySelectorAll("td")[columnIndex].remove();
|
||||
}
|
||||
|
||||
output.value = serializeTable(table);
|
||||
itemContextMenu.close();
|
||||
}, {passive: true});
|
||||
|
||||
insertRowAbove.addEventListener("click", function() {
|
||||
let rowIndex = parseInt(itemContextMenu.dataset.rowIndex, 10);
|
||||
|
||||
let newRow = new Array(table.columns.length);
|
||||
newRow.fill("");
|
||||
table.rows.insert(rowIndex, newRow);
|
||||
|
||||
tableElement.insertBefore(createRowElement(newRow, false), tableElement.querySelectorAll("tr")[rowIndex]);
|
||||
|
||||
output.value = serializeTable(table);
|
||||
itemContextMenu.close();
|
||||
}, {passive: true});
|
||||
|
||||
insertRowBelow.addEventListener("click", function() {
|
||||
let rowIndex = parseInt(itemContextMenu.dataset.rowIndex, 10);
|
||||
|
||||
let newRow = new Array(table.columns.length);
|
||||
newRow.fill("");
|
||||
table.rows.insert(rowIndex + 1, newRow);
|
||||
|
||||
let rowElement = createRowElement(newRow, false);
|
||||
let referenceNode = rowIndex >= 0
|
||||
? tableElement.querySelectorAll("tr")[rowIndex]
|
||||
: tableElement.querySelector("th");
|
||||
tableElement.insertAfter(rowElement, referenceNode);
|
||||
|
||||
output.value = serializeTable(table);
|
||||
itemContextMenu.close();
|
||||
}, {passive: true});
|
||||
|
||||
insertColumnLeft.addEventListener("click", function() {
|
||||
let columnIndex = parseInt(itemContextMenu.dataset.columnIndex, 10);
|
||||
|
||||
let column = {
|
||||
text: "",
|
||||
leftAligned: false,
|
||||
rightAligned: false,
|
||||
maxLength: 3
|
||||
};
|
||||
table.columns.insert(columnIndex, column);
|
||||
for (let row of table.rows) {
|
||||
row.insert(columnIndex, "");
|
||||
}
|
||||
|
||||
let td = document.createElement("td");
|
||||
td.appendChild(createItemElement("", column));
|
||||
|
||||
let th = tableElement.querySelector("th");
|
||||
for (let tr of tableElement.querySelectorAll("th, tr")) {
|
||||
tr.insertBefore(td.cloneNode(true), tr.querySelectorAll("td")[columnIndex]);
|
||||
}
|
||||
|
||||
output.value = serializeTable(table);
|
||||
itemContextMenu.close();
|
||||
}, {passive: true});
|
||||
|
||||
insertColumnRight.addEventListener("click", function() {
|
||||
let columnIndex = parseInt(itemContextMenu.dataset.columnIndex, 10);
|
||||
|
||||
let column = {
|
||||
text: "",
|
||||
leftAligned: false,
|
||||
rightAligned: false,
|
||||
maxLength: 3
|
||||
};
|
||||
table.columns.insert(columnIndex + 1, column);
|
||||
for (let row of table.rows) {
|
||||
row.insert(columnIndex + 1, "");
|
||||
}
|
||||
|
||||
let td = document.createElement("td");
|
||||
td.appendChild(createItemElement("", column));
|
||||
|
||||
let th = tableElement.querySelector("th");
|
||||
for (let tr of tableElement.querySelectorAll("th, tr")) {
|
||||
tr.insertAfter(td.cloneNode(true), tr.querySelectorAll("td")[columnIndex]);
|
||||
}
|
||||
|
||||
output.value = serializeTable(table);
|
||||
itemContextMenu.close();
|
||||
}, {passive: true});
|
||||
|
||||
|
||||
|
||||
tableElement.addEventListener("input", function(event) {
|
||||
let {columnIndex, rowIndex} = findTableIndices(event.target);
|
||||
if (rowIndex < 0) {
|
||||
// is modifying a header
|
||||
table.columns[columnIndex].text = event.target.value;
|
||||
} else {
|
||||
// is modifying a row
|
||||
table.rows[rowIndex][columnIndex] = event.target.value;
|
||||
}
|
||||
|
||||
output.value = serializeTable(table);
|
||||
|
||||
table.columns[columnIndex].maxLength = calculateColumnLength(table.columns[columnIndex], table.rows, columnIndex);
|
||||
for (let rowElement of tableElement.querySelectorAll("th, tr")) {
|
||||
rowElement.children[columnIndex].querySelector("textarea").cols = table.columns[columnIndex].maxLength;
|
||||
}
|
||||
});
|
||||
|
||||
tableElement.addEventListener("contextmenu", function(event) {
|
||||
if (event.shiftKey || event.target.localName !== "textarea") {
|
||||
return;
|
||||
}
|
||||
|
||||
let {columnIndex, rowIndex} = findTableIndices(event.target);
|
||||
itemContextMenu.dataset.columnIndex = columnIndex;
|
||||
itemContextMenu.dataset.rowIndex = rowIndex;
|
||||
itemContextMenu.style.margin = 0;
|
||||
itemContextMenu.style.top = itemContextMenu.style.left = "0px";
|
||||
itemContextMenu.showModal();
|
||||
|
||||
let rect = itemContextMenu.getBoundingClientRect();
|
||||
let top = event.clientY;
|
||||
let left = event.clientX;
|
||||
|
||||
if (top + rect.height > window.innerHeight) {
|
||||
top = window.innerHeight - rect.height;
|
||||
}
|
||||
if (left + rect.width > window.innerWidth) {
|
||||
left = window.innerWidth - rect.width;
|
||||
}
|
||||
itemContextMenu.style.margin = 0;
|
||||
itemContextMenu.style.top = `${top}px`;
|
||||
itemContextMenu.style.left = `${left}px`;
|
||||
|
||||
let column = table.columns[columnIndex];
|
||||
if (column.leftAligned && column.rightAligned) {
|
||||
itemContextMenu.querySelector("#center").checked = true;
|
||||
} else if (column.leftAligned) {
|
||||
itemContextMenu.querySelector("#left").checked = true;
|
||||
} else if (column.rightAligned) {
|
||||
itemContextMenu.querySelector("#right").checked = true;
|
||||
} else {
|
||||
itemContextMenu.querySelector("#none").checked = true;
|
||||
}
|
||||
|
||||
deleteRow.disabled = event.target.closest("th") !== null;
|
||||
deleteColumn.disabled = table.columns.length < 2;
|
||||
insertRowAbove.disabled = event.target.closest("th") !== null;
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
|
||||
|
||||
document.querySelector("#copy-to-clipboard").addEventListener("click", function() {
|
||||
output.select();
|
||||
navigator.clipboard.writeText(output.value).then(
|
||||
function() {
|
||||
// success
|
||||
},
|
||||
function(reason) {
|
||||
// TODO: make visible to user
|
||||
console.error(`Failed to copy to clipboard: ${reason}`);
|
||||
}
|
||||
);
|
||||
}, {passive: true});
|
||||
|
||||
|
||||
|
||||
window.addEventListener("click", function(event) {
|
||||
if (event.target.localName !== "dialog") {
|
||||
return;
|
||||
}
|
||||
|
||||
let rect = event.target.getBoundingClientRect();
|
||||
if (rect.top <= event.clientY && rect.left <= event.clientX && rect.bottom >= event.clientY && rect.right >= event.clientX) {
|
||||
return;
|
||||
}
|
||||
event.target.close();
|
||||
}, {passive: true});
|
||||
|
||||
reloadTable();
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* str: string
|
||||
*
|
||||
* Returns a string
|
||||
*/
|
||||
function escapeItem(str) {
|
||||
return str.replaceAll(/(\\*)\|/g, function(match, backslashes) {
|
||||
if (backslashes.length % 2 === 0) {
|
||||
backslashes += "\\";
|
||||
}
|
||||
return backslashes + "|";
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* escapedColumns: array of objects
|
||||
* escapedRow: array of string
|
||||
*
|
||||
* Returns a string
|
||||
*/
|
||||
function writeRow(escapedColumns, escapedRow) {
|
||||
let res = "";
|
||||
for (let i = 0; i < escapedColumns.length; i++) {
|
||||
let escapedColumn = escapedColumns[i];
|
||||
let escapedItem = escapedRow[i];
|
||||
res += `| ${escapedItem}${" ".repeat(escapedColumn.maxLength - escapedItem.length)} `;
|
||||
}
|
||||
res += "|";
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/*
|
||||
* escapedColumns: array of objects
|
||||
*
|
||||
* Returns a string
|
||||
*/
|
||||
function writeHeader(escapedColumns) {
|
||||
let headerRow = [];
|
||||
let delimiterRow = [];
|
||||
|
||||
for (let escapedColumn of escapedColumns) {
|
||||
headerRow.push(escapedColumn.text);
|
||||
|
||||
let delimiter = "";
|
||||
if (escapedColumn.leftAligned) {
|
||||
delimiter += ":";
|
||||
}
|
||||
delimiter += "-".repeat(escapedColumn.maxLength - escapedColumn.leftAligned - escapedColumn.rightAligned);
|
||||
if (escapedColumn.rightAligned) {
|
||||
delimiter += ":";
|
||||
}
|
||||
delimiterRow.push(delimiter);
|
||||
}
|
||||
|
||||
return writeRow(escapedColumns, headerRow) + "\n" + writeRow(escapedColumns, delimiterRow);
|
||||
}
|
||||
|
||||
/*
|
||||
* column: object
|
||||
* rows: array of array of strings
|
||||
* headerIndex: integer
|
||||
*
|
||||
* Returns an integer
|
||||
*/
|
||||
function calculateColumnLength(column, rows, columnIndex) {
|
||||
let length = Math.max(column.text.length, 3);
|
||||
|
||||
for (let row of rows) {
|
||||
let item = row[columnIndex];
|
||||
if (item.length > length) {
|
||||
length = item.length;
|
||||
}
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
/*
|
||||
* table: object
|
||||
*
|
||||
* Returns a string
|
||||
*/
|
||||
function serializeTable(table) {
|
||||
let escapedColumns = [];
|
||||
let escapedRows = [];
|
||||
|
||||
for (let column of table.columns) {
|
||||
escapedColumns.push({
|
||||
text: escapeItem(column.text),
|
||||
leftAligned: column.leftAligned,
|
||||
rightAligned: column.rightAligned
|
||||
});
|
||||
}
|
||||
|
||||
for (let row of table.rows) {
|
||||
let escapedRow = [];
|
||||
for (let item of row) {
|
||||
escapedRow.push(escapeItem(item));
|
||||
}
|
||||
|
||||
escapedRows.push(escapedRow);
|
||||
}
|
||||
|
||||
for (let i = 0; i < escapedColumns.length; i++) {
|
||||
escapedColumns[i].maxLength = calculateColumnLength(escapedColumns[i], escapedRows, i);
|
||||
}
|
||||
|
||||
let res = writeHeader(escapedColumns);
|
||||
for (let escapedRow of escapedRows) {
|
||||
res += "\n" + writeRow(escapedColumns, escapedRow);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// XXX: perhaps calculateColumnLength could be placed somewhere else?
|
||||
export {serializeTable, calculateColumnLength};
|
|
@ -0,0 +1,68 @@
|
|||
html:has(dialog[open]) {
|
||||
overflow: hidden;
|
||||
}
|
||||
/* prevent table from wrapping when contents are too long */
|
||||
table {
|
||||
min-width: max-content;
|
||||
}
|
||||
/* prevent textboxes from soft-wrapping */
|
||||
textarea {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 0;
|
||||
}
|
||||
td {
|
||||
display: inline;
|
||||
}
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
.left {
|
||||
text-align: left;
|
||||
}
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
.left.right {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#load-dialog h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
#load-dialog #error {
|
||||
display: block;
|
||||
}
|
||||
#load-dialog textarea {
|
||||
width: 80vw;
|
||||
height: 75vh;
|
||||
}
|
||||
#load-dialog .buttons {
|
||||
text-align: right;
|
||||
}
|
||||
#load-dialog form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#item-context-menu br {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
}
|
||||
#item-context-menu button {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: scroll;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
th textarea {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#output {
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import {deserializeTable} from "./deserialize_table.mjs";
|
||||
import {serializeTable, calculateColumnLength} from "./serialize_table.mjs";
|
||||
|
||||
export {deserializeTable, serializeTable, calculateColumnLength};
|
Loading…
Reference in New Issue