add named destination support

thanks typst contributors for adding this in v0.11.0
This commit is contained in:
dogeystamp 2024-03-22 20:29:34 -04:00
parent 0135f1eb4f
commit 585ba739d1
Signed by: dogeystamp
GPG Key ID: 7225FE3592EFFA38
6 changed files with 64 additions and 12 deletions

View File

@ -47,10 +47,13 @@ or add the following to your `.config/zathura/zathurarc`:
``` ```
map <C-l> exec copy_ref map <C-l> exec copy_ref
map <C-g> exec "copy_ref --section" map <C-g> exec "copy_ref --section"
map <C-k> exec "copy_ref --destination"
``` ```
This will make Ctrl-L copy a reference to the current page, This will make Ctrl-L copy a reference to the current page,
and Ctrl-G copy a reference to a specific section in Zathura. Ctrl-G copy a reference to a specific section title,
and Ctrl-K copy a reference to a [named destination](https://tex.stackexchange.com/questions/213860/how-to-generate-a-named-destination-in-pdf).
Destinations and sections will show up in a rofi menu for you to select.
## limitations ## limitations
@ -66,5 +69,5 @@ and I can not make any guarantees.
Also: Also:
- Section references are unreliable because titles might change, - Section references are unreliable because titles might change,
and there might be sections with the same title. and there might be sections with the same title.
Proper IDs for bookmarks are possible, Use named destinations for documents that you built yourself (e.g. using Typst, LaTeX), and page numbers for external documents.
but not until Typst resolves [issue #1352](https://github.com/typst/typst/issues/1352). Only use section title references if your type-setting system does not support named destinations.

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from pdf_data import get_page_pdf, get_section_pdf from pdf_data import get_destination_pdf, get_page_pdf, get_section_pdf
from datatypes import * from datatypes import *
from enum import Enum, auto from enum import Enum, auto
from util import notify from util import notify
@ -9,7 +9,9 @@ import argparse
import formatter.typst as typst_fmt import formatter.typst as typst_fmt
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--section", help="Copy reference to the section title instead of the page number.", action="store_true") dest_group = parser.add_mutually_exclusive_group()
dest_group.add_argument("--section", help="Copy reference to the section title instead of the page number.", action="store_true")
dest_group.add_argument("--destination", help="Copy reference to a named destination instead of the page number.", action="store_true")
class LinkFormat(Enum): class LinkFormat(Enum):
@ -37,6 +39,8 @@ if __name__ == "__main__":
if args.section: if args.section:
ref = get_section_pdf() ref = get_section_pdf()
elif args.destination:
ref = get_destination_pdf()
else: else:
ref = get_page_pdf() ref = get_page_pdf()
@ -48,3 +52,5 @@ if __name__ == "__main__":
notify("Copied ref", f"{ref.filepath.name} p. {ref.page}") notify("Copied ref", f"{ref.filepath.name} p. {ref.page}")
case PDFSection(): case PDFSection():
notify("Copied ref", f"{ref.filepath.name} sec. {ref.title}") notify("Copied ref", f"{ref.filepath.name} sec. {ref.title}")
case PDFDestination():
notify("Copied ref", f"{ref.filepath.name} {ref.name}")

View File

@ -1,4 +1,4 @@
from typing import NewType, Union from typing import NewType, TypedDict, Union
from pathlib import Path from pathlib import Path
from dataclasses import dataclass from dataclasses import dataclass
@ -60,15 +60,33 @@ class PDFSection(_PDFReference):
title: SectionTitle title: SectionTitle
PDFReference = Union[PDFPage, PDFSection] @dataclass
class PDFDestination(_PDFReference):
"""Reference to a named destination in a PDF.
Attributes
----------
name
Destination name.
"""
name: str
PDFReference = Union[PDFPage, PDFSection, PDFDestination]
# for now no other format is implemented # for now no other format is implemented
# replace this with an union if that happens # replace this with an union if that happens
Reference = PDFReference Reference = PDFReference
# PyMuPDF type # PyMuPDF types
@dataclass @dataclass
class FitzBookmark: class FitzBookmark:
level: int level: int
title: SectionTitle title: SectionTitle
page: PageNumber page: PageNumber
class FitzDestinations(TypedDict):
page: PageNumber
to: tuple[int, int]
zoom: float

View File

@ -1,6 +1,6 @@
from os import environ from os import environ
from urllib.parse import urlencode from urllib.parse import urlencode
from datatypes import PDFPage, PDFSection, PDFReference, Reference from datatypes import PDFDestination, PDFPage, PDFSection, PDFReference, Reference
from typing import assert_never from typing import assert_never
from pathlib import Path from pathlib import Path
@ -28,6 +28,8 @@ def format_pdf_link(ref: PDFReference) -> str:
case PDFSection(): case PDFSection():
params["section"] = ref.title params["section"] = ref.title
default_label = ref.title default_label = ref.title
case PDFDestination():
params["destination"] = ref.name
case _ as obj: case _ as obj:
assert_never(obj) assert_never(obj)

View File

@ -16,9 +16,23 @@ def get_section_pdf() -> PDFSection:
rofi_res = rofi([f"{x.title}" for x in page_headers], prompt="Select header: ") rofi_res = rofi([f"{x.title}" for x in page_headers], prompt="Select header: ")
if rofi_res is None or rofi_res.index is None: if rofi_res is None or rofi_res.index is None:
raise RuntimeError("No header was selected.") raise RuntimeError("No header was selected.")
selected_header = page_headers[rofi_res.index] else:
selected_header = page_headers[rofi_res.index]
return PDFSection(filepath=page_ref.filepath, title=selected_header.title)
return PDFSection(filepath=page_ref.filepath, title=selected_header.title)
def get_destination_pdf() -> PDFDestination:
page_ref: PDFPage = get_page_pdf()
with fitz.Document(page_ref.filepath) as doc:
destinations = cast(FitzDestinations, cast(Any, doc).resolve_names())
page_dests = {k: x for k, x in destinations.items() if cast(Any, x)["page"]+1 == page_ref.page}
rofi_res = rofi([f"{k}" for k, _ in page_dests.items()], prompt="Select named destination: ")
if rofi_res is None or rofi_res.index is None:
raise RuntimeError("No destination was selected.")
else:
selected_header = [x for x in page_dests.items()][rofi_res.index]
return PDFDestination(filepath=page_ref.filepath, name=selected_header[0])
def get_page_pdf() -> PDFPage: def get_page_pdf() -> PDFPage:

View File

@ -14,6 +14,7 @@ query = parse_qs(url.query)
page: PageNumber = PageNumber(int(query.get("page", ["0"])[0])) page: PageNumber = PageNumber(int(query.get("page", ["0"])[0]))
section_list = query.get("section", []) section_list = query.get("section", [])
destination_list = query.get("destination", [])
if section_list != []: if section_list != []:
section: SectionTitle = SectionTitle(section_list[0]) section: SectionTitle = SectionTitle(section_list[0])
@ -25,8 +26,16 @@ if section_list != []:
else: else:
if len(headers) > 1: if len(headers) > 1:
notify("", f"Multiple sections '{section}' found: page might be incorrect") notify("", f"Multiple sections '{section}' found: page might be incorrect")
page = headers[0].page page = headers[0].page
elif destination_list != []:
destination_name = destination_list[0]
with fitz.Document(url.path) as doc:
destinations = FitzDestinations(cast(Any, doc).resolve_names())
destination = destinations.get(destination_name)
if not destination:
notify("", f"Failed to find named destination '{destination_name}': did the document change?")
else:
page = destination["page"]+1
subprocess.run(["zathura", "--page", str(page), url.path], text=True) subprocess.run(["zathura", "--page", str(page), url.path], text=True)