From cb08db6290ef82d5e2ba60017a549825657abe28 Mon Sep 17 00:00:00 2001 From: Grzegorz Godlewski Date: Sun, 23 Jun 2024 20:05:47 +0200 Subject: [PATCH] Fix anchor links to sections of documents Resolves: #472 --- src/containers/transform/LocalLinks.ts | 4 +-- .../transform/TransformContainer.ts | 10 ++++--- .../generateDocumentFrontMatter.ts | 4 +-- src/odt/LibreOffice.ts | 8 ++--- src/odt/MarkdownNodes.ts | 3 +- src/odt/OdtToMarkdown.ts | 29 +++++++++++-------- src/odt/markdownNodesUtils.ts | 4 +++ src/odt/postprocess/rewriteHeaders.ts | 25 +++++----------- src/utils/idParsers.ts | 8 +++++ 9 files changed, 51 insertions(+), 44 deletions(-) diff --git a/src/containers/transform/LocalLinks.ts b/src/containers/transform/LocalLinks.ts index b1eb47e0..b82fe7cb 100644 --- a/src/containers/transform/LocalLinks.ts +++ b/src/containers/transform/LocalLinks.ts @@ -1,5 +1,5 @@ import {FileContentService} from '../../utils/FileContentService.ts'; -import {FileId} from '../../model/model.js'; +import {FileId} from '../../model/model.ts'; interface Link { fileId: string; @@ -78,7 +78,7 @@ export class LocalLinks { if (link.fileId === fileId) { const links = link.links .filter(link => link.startsWith('gdoc:')) - .map(link => link.substring('gdoc:'.length)); + .map(link => link.substring('gdoc:'.length).replace(/#.*/, '')); return links.map(fileId => ({ fileId, diff --git a/src/containers/transform/TransformContainer.ts b/src/containers/transform/TransformContainer.ts index 8f82d516..cae4c7c5 100644 --- a/src/containers/transform/TransformContainer.ts +++ b/src/containers/transform/TransformContainer.ts @@ -24,6 +24,7 @@ import {MarkdownTreeProcessor} from './MarkdownTreeProcessor.ts'; import {LunrIndexer} from '../search/LunrIndexer.ts'; import {JobManagerContainer} from '../job/JobManagerContainer.ts'; import {UserConfigService} from '../google_folder/UserConfigService.ts'; +import {getUrlHash} from '../../utils/idParsers.ts'; const __filename = fileURLToPath(import.meta.url); @@ -342,10 +343,10 @@ export class TransformContainer extends Container { processed.add(fileId); const backLinks = this.localLinks.getBackLinks(fileId); for (const backLink of backLinks) { - if (processed.has(backLink)) { + if (processed.has(backLink.fileId)) { continue; } - filterFilesIds.add(backLink); + filterFilesIds.add(backLink.fileId); } } if (filterFilesIds.size > 0) { @@ -426,7 +427,8 @@ export class TransformContainer extends Container { if (fileName.endsWith('.md') || fileName.endsWith('.svg')) { const content = await destinationDirectory.readFile(fileName); const newContent = content.replace(/(gdoc:[A-Z0-9_-]+)/ig, (str: string) => { - const fileId = str.substring('gdoc:'.length); + const fileId = str.substring('gdoc:'.length).replace(/#.*/, ''); + const hash = getUrlHash(str); const lastLog = this.localLog.findLastFile(fileId); if (lastLog && lastLog.event !== 'removed') { if (fileName.endsWith('.svg')) { @@ -435,7 +437,7 @@ export class TransformContainer extends Container { return convertToRelativeMarkDownPath(lastLog.filePath, destinationDirectory.getVirtualPath() + fileName); } } else { - return 'https://drive.google.com/open?id=' + fileId; + return 'https://drive.google.com/open?id=' + fileId + hash.replace('#_', '#heading=h.'); } }); if (content !== newContent) { diff --git a/src/containers/transform/frontmatters/generateDocumentFrontMatter.ts b/src/containers/transform/frontmatters/generateDocumentFrontMatter.ts index 7de0500f..066f48c4 100644 --- a/src/containers/transform/frontmatters/generateDocumentFrontMatter.ts +++ b/src/containers/transform/frontmatters/generateDocumentFrontMatter.ts @@ -1,7 +1,7 @@ import yaml from 'js-yaml'; -import {MdFile} from '../../../model/LocalFile'; -import {FRONTMATTER_DUMP_OPTS} from './frontmatter'; +import {MdFile} from '../../../model/LocalFile.ts'; +import {FRONTMATTER_DUMP_OPTS} from './frontmatter.ts'; export function generateDocumentFrontMatter(localFile: MdFile, links: string[], fm_without_version = false) { diff --git a/src/odt/LibreOffice.ts b/src/odt/LibreOffice.ts index b38243c7..f071589e 100644 --- a/src/odt/LibreOffice.ts +++ b/src/odt/LibreOffice.ts @@ -76,7 +76,8 @@ export class TextLink implements ParagraphSection { @XmlElement() @XmlAttribute('text:name', 'name') -export class TextBookmark { +export class TextBookmark implements ParagraphSection { + type = 'bookmark'; name: string; } @@ -200,7 +201,7 @@ export class TextChangeEnd { @XmlElement() @XmlText('list', {isArray: true}) @XmlAttribute('text:style-name', 'styleName') -@XmlElementChild('text:bookmark', 'bookmark', 'TextBookmark') +@XmlElementChild('text:bookmark', 'list', 'TextBookmark', {isArray: true}) @XmlElementChild('text:a', 'list', 'TextLink', {isArray: true}) @XmlElementChild('text:span', 'list', 'TextSpan', {isArray: true}) @XmlElementChild('draw:rect', 'list', 'DrawRect', {isArray: true}) @@ -215,8 +216,7 @@ export class TextChangeEnd { @XmlElementChild('text:change-end', 'list', 'TextChangeEnd', {isArray: true}) export class TextParagraph implements TextSection { type = 'paragraph'; - bookmark: TextBookmark; - list: Array = []; + list: Array = []; annotations: OfficeAnnotation[] = []; styleName: string; } diff --git a/src/odt/MarkdownNodes.ts b/src/odt/MarkdownNodes.ts index fe8679b1..557ecf9f 100644 --- a/src/odt/MarkdownNodes.ts +++ b/src/odt/MarkdownNodes.ts @@ -16,7 +16,7 @@ export type TAG = 'BODY' | 'HR/' | 'B' | 'I' | 'BI' | 'BLANK/' | // | '/B' | '/I 'EMB_SVG' | 'EMB_SVG_G' | 'EMB_SVG_P/' | 'EMB_SVG_TEXT' | // | '/EMB_SVG' | '/EMB_SVG_G' | '/EMB_SVG_TEXT' 'EMB_SVG_TSPAN' | // | '/EMB_SVG_TSPAN' 'MATHML' | - 'CHANGE_START' | 'CHANGE_END' | 'RAW_MODE/' | 'HTML_MODE/' | 'MD_MODE/' | 'MACRO_MODE/' | 'COMMENT'; + 'CHANGE_START' | 'CHANGE_END' | 'RAW_MODE/' | 'HTML_MODE/' | 'MD_MODE/' | 'MACRO_MODE/' | 'COMMENT' | 'BOOKMARK/'; export interface TagPayload { lang?: string; @@ -34,7 +34,6 @@ export interface TagPayload { listStyle?: ListStyle; continueNumbering?: boolean; listLevel?: number; - bookmarkName?: string; pathD?: string; x?: number; diff --git a/src/odt/OdtToMarkdown.ts b/src/odt/OdtToMarkdown.ts index 4c2c633e..e8593ae1 100644 --- a/src/odt/OdtToMarkdown.ts +++ b/src/odt/OdtToMarkdown.ts @@ -10,7 +10,7 @@ import { TableCell, TableOfContent, TableRow, - TableTable, + TableTable, TextBookmark, TextLink, TextList, TextParagraph, @@ -18,7 +18,7 @@ import { TextSpace, TextSpan } from './LibreOffice.ts'; -import {urlToFolderId} from '../utils/idParsers.ts'; +import {getUrlHash, urlToFolderId} from '../utils/idParsers.ts'; import {MarkdownNodes, MarkdownTagNode} from './MarkdownNodes.ts'; import {inchesToPixels, inchesToSpaces, spaces} from './utils.ts'; import {extractPath} from './extractPath.ts'; @@ -230,13 +230,14 @@ export class OdtToMarkdown { async linkToText(currentTagNode: MarkdownTagNode, link: TextLink): Promise { let href = link.href; const id = urlToFolderId(href); + const hash = getUrlHash(link.href); if (id) { href = 'gdoc:' + id; } - this.addLink(href); + this.addLink(href + hash); - const block = this.chunks.createNode('A', { href: href }); + const block = this.chunks.createNode('A', { href: href + hash }); this.chunks.append(currentTagNode, block); currentTagNode = block; @@ -458,38 +459,36 @@ export class OdtToMarkdown { return false; } - async paragraphToText(currentTagNode: MarkdownTagNode, paragraph: TextParagraph): Promise { const style = this.getStyle(paragraph.styleName); const listStyle = this.getListStyle(style.listStyleName); - const bookmarkName = paragraph.bookmark?.name || null; if (this.hasStyle(paragraph, 'Heading_20_1')) { - const header = this.chunks.createNode('H1', { marginLeft: inchesToSpaces(style.paragraphProperties?.marginLeft), style, listStyle, bookmarkName }); + const header = this.chunks.createNode('H1', { marginLeft: inchesToSpaces(style.paragraphProperties?.marginLeft), style, listStyle }); this.chunks.append(currentTagNode, header); currentTagNode = header; } else if (this.hasStyle(paragraph, 'Heading_20_2')) { - const header = this.chunks.createNode('H2', { marginLeft: inchesToSpaces(style.paragraphProperties?.marginLeft), style, listStyle, bookmarkName }); + const header = this.chunks.createNode('H2', { marginLeft: inchesToSpaces(style.paragraphProperties?.marginLeft), style, listStyle }); this.chunks.append(currentTagNode, header); currentTagNode = header; } else if (this.hasStyle(paragraph, 'Heading_20_3')) { - const header = this.chunks.createNode('H3', { marginLeft: inchesToSpaces(style.paragraphProperties?.marginLeft), style, listStyle, bookmarkName }); + const header = this.chunks.createNode('H3', { marginLeft: inchesToSpaces(style.paragraphProperties?.marginLeft), style, listStyle }); this.chunks.append(currentTagNode, header); currentTagNode = header; } else if (this.hasStyle(paragraph, 'Heading_20_4')) { - const header = this.chunks.createNode('H4', { marginLeft: inchesToSpaces(style.paragraphProperties?.marginLeft), style, listStyle, bookmarkName }); + const header = this.chunks.createNode('H4', { marginLeft: inchesToSpaces(style.paragraphProperties?.marginLeft), style, listStyle }); this.chunks.append(currentTagNode, header); currentTagNode = header; } else if (this.isCourier(paragraph.styleName)) { - const block = this.chunks.createNode('PRE', { marginLeft: inchesToSpaces(style.paragraphProperties?.marginLeft), style, listStyle, bookmarkName }); + const block = this.chunks.createNode('PRE', { marginLeft: inchesToSpaces(style.paragraphProperties?.marginLeft), style, listStyle }); this.chunks.append(currentTagNode, block); currentTagNode = block; } else { - const block = this.chunks.createNode('P', { marginLeft: inchesToSpaces(style.paragraphProperties?.marginLeft), style, listStyle, bookmarkName }); + const block = this.chunks.createNode('P', { marginLeft: inchesToSpaces(style.paragraphProperties?.marginLeft), style, listStyle }); this.chunks.append(currentTagNode, block); currentTagNode = block; } @@ -596,6 +595,12 @@ export class OdtToMarkdown { case 'change_end': this.chunks.append(currentTagNode, this.chunks.createNode('CHANGE_END')); break; + case 'bookmark': + { + const bookmark = child; + this.chunks.append(currentTagNode, this.chunks.createNode('BOOKMARK/', { id: bookmark.name })); + } + break; } } } diff --git a/src/odt/markdownNodesUtils.ts b/src/odt/markdownNodesUtils.ts index 1fc3c8da..89ff83fc 100644 --- a/src/odt/markdownNodesUtils.ts +++ b/src/odt/markdownNodesUtils.ts @@ -221,6 +221,8 @@ function chunkToText(chunk: MarkdownNode, ctx: ToTextContext) { return addLiNumbers(chunk, ctx, chunksToText(chunk.children, { ...ctx, inListItem: true, parentLevel: chunk.payload.listLevel })); case 'TOC': return chunksToText(chunk.children, ctx); // TODO + case 'BOOKMARK/': + return ``; } return chunksToText(chunk.children, ctx); case 'html': @@ -296,6 +298,8 @@ function chunkToText(chunk: MarkdownNode, ctx: ToTextContext) { const fontSize = inchesToPixels(chunk.payload.style?.textProperties.fontSize); return `` + chunksToText(chunk.children, ctx) + '\n'; } + case 'BOOKMARK/': + return ``; } return chunksToText(chunk.children, ctx); default: diff --git a/src/odt/postprocess/rewriteHeaders.ts b/src/odt/postprocess/rewriteHeaders.ts index 0a5ba35d..0a6f66f7 100644 --- a/src/odt/postprocess/rewriteHeaders.ts +++ b/src/odt/postprocess/rewriteHeaders.ts @@ -1,27 +1,16 @@ -import slugify from 'slugify'; -import {extractText, walkRecursiveAsync, walkRecursiveSync} from '../markdownNodesUtils.ts'; +import {walkRecursiveAsync} from '../markdownNodesUtils.ts'; import {MarkdownNodes} from '../MarkdownNodes.ts'; export async function rewriteHeaders(markdownChunks: MarkdownNodes) { - const headersMap = {}; - await walkRecursiveAsync(markdownChunks.body, async (chunk) => { - if (chunk.isTag === true && ['H1', 'H2', 'H3', 'H4'].includes(chunk.tag)) { // && 'md' === this.currentMode) { - if (chunk.payload.bookmarkName) { - const innerTxt = extractText(chunk); - const slug = slugify(innerTxt.trim(), { replacement: '-', lower: true, remove: /[#*+~.()'"!:@]/g }); - if (slug) { - headersMap['#' + chunk.payload.bookmarkName] = '#' + slug; + if (chunk.isTag && ['H1', 'H2', 'H3', 'H4'].includes(chunk.tag)) { + if (chunk.children.length > 1) { + const first = chunk.children[0]; + if (first.isTag && first.tag === 'BOOKMARK/') { + const toMove = chunk.children.splice(0, 1); + chunk.children.splice(chunk.children.length, 0, ...toMove); } } } }); - - walkRecursiveSync(markdownChunks.body, (chunk) => { - if (chunk.isTag === true && chunk.payload?.href) { - if (headersMap[chunk.payload.href]) { - chunk.payload.href = headersMap[chunk.payload.href]; - } - } - }); } diff --git a/src/utils/idParsers.ts b/src/utils/idParsers.ts index 9a7ab0f2..1624ad01 100644 --- a/src/utils/idParsers.ts +++ b/src/utils/idParsers.ts @@ -71,3 +71,11 @@ export function urlToFolderId(url: string): string | null { return null; } + +export function getUrlHash(url: string): string { + const idx = url.indexOf('#'); + if (idx >= 0) { + return url.substring(idx).replace('#heading=h.', '#_'); + } + return ''; +}