Skip to content

Commit

Permalink
fix the headerRE
Browse files Browse the repository at this point in the history
  • Loading branch information
PlayerMiller109 committed Dec 29, 2024
0 parents commit 1fbf841
Show file tree
Hide file tree
Showing 4 changed files with 380 additions and 0 deletions.
30 changes: 30 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
name: Bug report
about: Report a bug
title: ''
labels: ''
assignees: ''

---

**Check before the Report**

- [ ] I have verified that I am on the latest version of the plugin.
- [ ] I have tested in Sandbox Vault.

If you know Chinese:
如果你懂中文,请参照 [故障排查指导 Troubleshooting Guide](https://forum-zh.obsidian.md/t/topic/27879/1) 先行排查。

else, you can refer to: [About the Bug reports category - Obsidian Forum](https://forum.obsidian.md/t/about-the-bug-reports-category/24/11)
<!--
On desktop, open the sandbox vault (Open Help > Sandbox Vault, this can be accessed from the command palette or the lower left ribbon) and see if you can reproduce the issue.
-->

**Describe the Bug**


**How to Reproduce**
<!--
If applicable, add screenshots, GIFs and attachments to help explain your problem.
-->

53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
Preamble see [Obsidian Forum t84520](https://forum.obsidian.md/t/mini-plugin-sheets-basic-merge-markdown-table-cells-in-editing-mode/84520/1) 中文说明见 [Obsidian Chinese Forum t35091](https://forum-zh.obsidian.md/t/topic/35091/1)

- In the current version you need to switch to reading mode before exporting a PDF.
- Use the plugin command 'rebuildCurrent' (default hotkey `F5`) in editing mode to refresh.
- It is recommended to refresh once before exporting a PDF, then switch to reading mode and export.
- When used outside tables, it will refresh the active leaf.
- When used in a normal table cell, it will refresh the table. Avoid placing your cursor in a signifier cell.
- When used in a merged table cell, it will unmerge the cell, and the cell will become a normal cell.
- Do not use the up Sign in the first row of the table body; that is, do not merge the table header and the table body.

<details>
<summary>Test text, click to unfold</summary>

````markdown
| head1 | < |
| ----- | ------ |
| | table1 |
| | ^ |

> | head2 | < |
> | ----- | ------ |
> | | table2 |
> | | ^ |
>
> | head3 | < |
> | ----- | ------ |
> | | table3 |
> | | ^ |

> [!quote]
> | head4 | < |
> | ----- | ------ |
> | | table4 |
> | | ^ |
>
> | head5 | < |
> | ----- | ------ |
> | | table5 |
> | | ^ |

```sheet
| head6 | < |
| ----- | ------ |
| | table6 |
| | ^ |
```
````
</details>

(2024-10-16) Test in Obsidian v1.6.7 Sandbox Vault:

<image width="420" src="https://github.com/user-attachments/assets/d226b8da-c887-4c03-9276-a96879b1f91a">
<br><sup>left: Live Preview; right: Reading Mode</sup>
287 changes: 287 additions & 0 deletions main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
const Sign = {up: '^', left: '<'}, tableId = 'obsidian-sheet'
const import_lazyParsers = (app, ob)=> {
class SheetElement extends ob.MarkdownRenderChild {
constructor(el, source, isBlock) {
super(el)
const tableEl = isBlock ? el.createEl('table') : el
tableEl.id = tableId
this.tableHead = tableEl.createEl('thead')
this.tableBody = tableEl.createEl('tbody')
this.contentGrid = source.split('\n').filter(row=> row).map(row=>
row.split(this.cellBorderRE).slice(1, -1).map(cell=> cell.trim())
)
}
cellBorderRE = /(?<!\\)\|/
headerRE = /^\s*?(\:)?(?:-+)(\:)?\s*/
rowMaxLength = 0; domGrid = []
onload() {
this.normalizeGrid()
this.headerRow = this.contentGrid.findIndex(row=> row.every(col=> this.headerRE.test(col)))
if (this.headerRow !== -1)
this.colStyles = this.getHeaderStyles(this.contentGrid[this.headerRow])
this.buildDomTable()
}
onunload() {}
normalizeGrid() {
for (let rowIndex = 0; rowIndex < this.contentGrid.length; rowIndex++) {
const rows = this.contentGrid[rowIndex]
if (this.rowMaxLength < rows.length) this.rowMaxLength = rows.length
}
this.contentGrid = this.contentGrid.map(line=> Array.from(
{ ...line, length: this.rowMaxLength }, cell=> cell || ''
))
}

getHeaderStyles(heads) {
return heads.map(head=> {
const alignment = head.match(this.headerRE), styles = {}
if (alignment[1] && alignment[2]) styles['textAlign'] = 'center';
else if (alignment[1]) styles['textAlign'] = 'left';
else if (alignment[2]) styles['textAlign'] = 'right';
return { styles }
})
}

buildDomTable() {
for (let rowIndex = 0; rowIndex < this.contentGrid.length; rowIndex++) {
if (rowIndex == this.headerRow) continue
let rowNode = this.tableBody.createEl('tr')
if (rowIndex < this.headerRow) rowNode = this.tableHead.createEl('tr')
this.domGrid[rowIndex] = []
const rows = this.contentGrid[rowIndex]
for (let colIndex = 0; colIndex < rows.length; colIndex++)
this.buildDomCell(rowIndex, colIndex, rowNode)
}
}
buildDomCell(rowIndex, colIndex, rowNode) {
if (rowIndex == this.headerRow) return
let cellTag = 'td', cell, cellStyles
if (rowIndex < this.headerRow) cellTag = 'th'
const cellContent = this.contentGrid[rowIndex][colIndex]
if (cellContent == Sign.left && colIndex > 0) {
cell = this.domGrid[rowIndex][colIndex - 1]
cell.colSpan || Object.assign(cell, { colSpan: 1 })
cell.colSpan += 1
} else if (cellContent == Sign.up && rowIndex > 0) {
cell = this.domGrid[rowIndex - 1][colIndex]
cell.rowSpan || Object.assign(cell, { rowSpan: 1 })
if (rowIndex - 1 > cell.rowSpan) cell.rowSpan += 1
} else {
cell = rowNode.createEl(cellTag)
ob.MarkdownRenderer.render(
app, cellContent||'\u200B', cell, '', this
).then(()=> {
const isP = el=> el.tagName == 'P';
[cell.firstChild, cell.lastChild].map(el=> {
if (!isP(el)) return
if (!el.textContent && !el.children[0])
el.remove()
})
let _ihtml = ''
for (const node of cell.childNodes) {
if (node.nodeType === 3) _ihtml += node.data;
else _ihtml += isP(node) ? node.innerHTML : node.outerHTML
}
cell.innerHTML = _ihtml
})
}
if (this.colStyles?.[colIndex]) {
cellStyles = { ...cellStyles, ...this.colStyles[colIndex].styles }
}
Object.assign(cell.style, cellStyles)
return this.domGrid[rowIndex][colIndex] = cell
}
}
// trim content before |
const trimLeading = line=> line.replace(/^.*?(?=(?<!\\)\|)/, '')
// yes ? match ^| line : match not ^| line
const fI = (arr, yes)=> arr.findIndex(i=> (yes ? /^\|/ : /^(?!\|)/).test(i))
const rgxFindTable = (prev, rowSources)=> {
if (rowSources[0].startsWith('```')) return // exclude codeblock
rowSources.splice(0, fI(rowSources, !0))
let endIndex = fI(rowSources)
while (endIndex > -1) {
prev.r = rowSources.splice(endIndex)
endIndex = rowSources[0].startsWith('|') ? -1 : fI(prev.r)
}
}
const postParser = new class {
source = []
main = (el, ctx)=> {
const view5 = app.workspace.getActiveFileView(); if (!view5) return
const tableEls = Array.from(el.querySelectorAll('table')); if (!tableEls[0]) return
const prev = {}
tableEls.map(async (tEl, tIndex)=> {
let source; const sec = ctx.getSectionInfo(tEl)
if (!sec) {
await new Promise(r=> setTimeout(r, 50))
const callout = tEl.offsetParent
if (callout?.cmView) { // for source mode, assume table is in callout
let rowSources
if (prev.callout === callout) rowSources = prev.r;
else {
const a1 = callout.cmView.widget.text; if (!a1) return // table in Dataview
rowSources = a1.split('\n').map(line=> trimLeading(line))
}
prev.callout = callout
rgxFindTable(prev, rowSources)
source = rowSources.join('\n')
} else source = this.source[tIndex] // when export
// reading mode
} else {
const { text, lineStart, lineEnd } = sec; let rowSources
if (prev.t == text && prev.s == lineStart && prev.ed == lineEnd) rowSources = prev.r; // continue old one
else rowSources = text.split('\n').slice(lineStart, lineEnd+1).map(line=> trimLeading(line)) // get new one
prev.t = text; prev.s = lineStart; prev.ed = lineEnd
rgxFindTable(prev, rowSources)
source = rowSources.join('\n')
this.source.push(source)
}
if (!source) return; tEl.empty(); ctx.addChild(new SheetElement(tEl, source))
})
}
}
return {
postParser,
blockParser: (source, el, ctx)=> source && ctx.addChild(new SheetElement(el, source, !0))
}
}
const import_handleFocus = ()=> {
const isSign = text=> Object.values(Sign).includes(text)
return table=> {
const _old = table.receiveCellFocus
table.receiveCellFocus = function(row, col, func, flag) {
if (table.rows[row]?.[col]?.el.style.display == 'none') {
const { cell } = table.editor.tableCell
, { row: maxRow, col: maxCol } = table.rows.flat().pop()
if (row === cell.row) {
while (isSign(table.rows[row]?.[col]?.text))
col += col < cell.col ? -1 : 1
if (col < 0) {
while (isSign(table.rows[row]?.[0].text)) row--
}
if (col > maxCol) {
col = 0; row++
if (row > maxRow) table.insertRow(row, col)
}
}
else if (col === cell.col) {
while (isSign(table.rows[row]?.[col]?.text))
row += row < cell.row ? -1 : 1
if (row < 0) {
while (isSign(table.rows[0][col]?.text)) col--
}
}
else {
if (row === cell.row - 1) {
while (isSign(table.rows[row][col]?.text)) col--
}
if (row === cell.row + 1) {
while (isSign(table.rows[row][col]?.text)) col++
}
}
}
_old.call(this, row, col, func, flag)
}
}
}
const import_sheet = (app, {ob, ViewPlugin})=> {
const handleFocus = import_handleFocus()
, disable = cell=> { cell.el.id = tableId; cell.el.style.display = 'none' }
, mergeTable = table=> {
const cells = table.rows.flat(); let cell
for (const _cell of cells) {
if (_cell.el.id == tableId) continue; let i = 1, breaked = !1
if (_cell.text == Sign.left && _cell.col > 0) {
disable(_cell)
do {
cell = cells.find(cell2=> cell2.row == _cell.row && cell2.col == _cell.col - i)
if (!cell || cell.text == Sign.up) { breaked = !0; break }; i++
} while (cell.el.id == tableId); if (breaked) continue
const { el: cellEl } = cell
cellEl.colSpan || Object.assgin(cellEl, { colSpan: 1 })
cellEl.colSpan += 1
} else if (_cell.text == Sign.up && _cell.row > 0) {
disable(_cell)
do {
cell = cells.find(cell2=> cell2.row == _cell.row - i && cell2.col == _cell.col)
if (!cell) { breaked = !0; break }; i++
} while (cell.el.id == tableId); if (breaked) continue
const { el: cellEl } = cell
cellEl.rowSpan || Object.assign(cellEl, { rowSpan: 1 })
cellEl.rowSpan += 1
}
}
handleFocus(table)
}
, mergeAllInView = view=> view.docView.children.flatMap(c=>
c.dom.className.includes('table-widget') ? c.widget : []
).map(mergeTable)
const getEMode = ()=> app.workspace.getActiveFileView()?.editMode
class liveParser {
update(update) {
const eMode = getEMode(); if (!eMode) return
const { tableCell } = eMode // when cursor in a table you can get tableCell
const undo = update.transactions.find(tr=> tr.isUserEvent('undo'))
// table.render() is an Ob prototype, you can use table.rebuildTable() too
if (undo && tableCell) { tableCell.table.render(); mergeTable(tableCell.table) }
const { view } = update
if (
update.focusChanged && view.hasFocus
|| update.viewportChanged
) setTimeout(()=> mergeAllInView(view))
}
}
const { postParser, blockParser } = import_lazyParsers(app, ob)
const updateMerge = ()=> {
postParser.source = []
const eMode = getEMode(); if (!eMode) return
const view = eMode.cm
if (view) setTimeout(()=> mergeAllInView(view), 50)
}
const unmergeCell = tableCell=> {
const { table, cell } = tableCell
, cells = table.rows.flat(), { row, col, el: cellEl } = cell
if (cellEl.rowSpan > 1 || cellEl.colSpan > 1) {
cells.filter(cell2=>
row <= cell2.row && cell2.row < row + cellEl.rowSpan
&& col <= cell2.col && cell2.col < col + cellEl.colSpan
).map(cell2=> {
cell2.el.removeAttribute('id')
cell2.el.style.display = 'table-cell'
})
cellEl.colSpan = cellEl.rowSpan = 1; return !0
}
}
return function() {
this.registerMarkdownPostProcessor(postParser.main)
this.registerMarkdownCodeBlockProcessor('sheet', blockParser)
this.registerEvent(app.workspace.on('file-open', updateMerge))
app.workspace.onLayoutReady(updateMerge)
this.addCommand({
id: 'rebuild', name: 'rebuildCurrent',
callback: async ()=> {
postParser.source = []
const eMode = getEMode(); if (!eMode) return
const { tableCell } = eMode
if (tableCell) {
const checking = unmergeCell(tableCell)
if (!checking) mergeTable(tableCell.table)
} else {
const leaves = app.workspace.getLeavesOfType('markdown')
.filter(leaf=> leaf.view.path == eMode.path)
for (const leaf of leaves) await leaf.rebuildView()
}
},
hotkeys: [{modifiers: [], key: 'F5'}]
})
this.registerEditorExtension([ViewPlugin.fromClass(liveParser)])
}
}
const ob = require('obsidian'), { ViewPlugin } = require('@codemirror/view')
module.exports = class extends ob.Plugin {
onload() {
import_sheet(this.app, {ob, ViewPlugin}).call(this)
}
onunload() {}
}
10 changes: 10 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": "sheets-basic",
"name": "Sheets Basic",
"version": "0.0.1",
"minAppVersion": "1.5.0",
"description": "Merge markdown table cells.",
"author": "PlayerMiller109",
"authorUrl": "https://github.com/PlayerMiller109/obsidian-sheets-basic",
"isDesktopOnly": false
}

0 comments on commit 1fbf841

Please sign in to comment.