diff --git a/README.md b/README.md index 1d68d72..a6cc217 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,13 @@ or add the following to your `.config/zathura/zathurarc`: ``` map exec copy_ref map exec "copy_ref --section" +map exec "copy_ref --destination" ``` 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 @@ -66,5 +69,5 @@ and I can not make any guarantees. Also: - Section references are unreliable because titles might change, and there might be sections with the same title. - Proper IDs for bookmarks are possible, - but not until Typst resolves [issue #1352](https://github.com/typst/typst/issues/1352). + Use named destinations for documents that you built yourself (e.g. using Typst, LaTeX), and page numbers for external documents. + Only use section title references if your type-setting system does not support named destinations. diff --git a/copy_ref b/copy_ref index da3c16b..c71655d 100755 --- a/copy_ref +++ b/copy_ref @@ -1,6 +1,6 @@ #!/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 enum import Enum, auto from util import notify @@ -9,7 +9,9 @@ import argparse import formatter.typst as typst_fmt 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): @@ -37,6 +39,8 @@ if __name__ == "__main__": if args.section: ref = get_section_pdf() + elif args.destination: + ref = get_destination_pdf() else: ref = get_page_pdf() @@ -48,3 +52,5 @@ if __name__ == "__main__": notify("Copied ref", f"{ref.filepath.name} p. {ref.page}") case PDFSection(): notify("Copied ref", f"{ref.filepath.name} sec. {ref.title}") + case PDFDestination(): + notify("Copied ref", f"{ref.filepath.name} {ref.name}") diff --git a/datatypes.py b/datatypes.py index 04ee889..0a44b60 100644 --- a/datatypes.py +++ b/datatypes.py @@ -1,4 +1,4 @@ -from typing import NewType, Union +from typing import NewType, TypedDict, Union from pathlib import Path from dataclasses import dataclass @@ -60,15 +60,33 @@ class PDFSection(_PDFReference): 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 # replace this with an union if that happens Reference = PDFReference -# PyMuPDF type +# PyMuPDF types @dataclass class FitzBookmark: level: int title: SectionTitle page: PageNumber + +class FitzDestinations(TypedDict): + page: PageNumber + to: tuple[int, int] + zoom: float diff --git a/formatter/typst.py b/formatter/typst.py index 94e462d..f9d7294 100644 --- a/formatter/typst.py +++ b/formatter/typst.py @@ -1,6 +1,6 @@ from os import environ 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 pathlib import Path @@ -28,6 +28,8 @@ def format_pdf_link(ref: PDFReference) -> str: case PDFSection(): params["section"] = ref.title default_label = ref.title + case PDFDestination(): + params["destination"] = ref.name case _ as obj: assert_never(obj) diff --git a/pdf_data.py b/pdf_data.py index a4c1f15..978eea7 100644 --- a/pdf_data.py +++ b/pdf_data.py @@ -16,9 +16,23 @@ def get_section_pdf() -> PDFSection: 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: 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: diff --git a/pdfref_handler b/pdfref_handler index c201335..261fc38 100755 --- a/pdfref_handler +++ b/pdfref_handler @@ -14,6 +14,7 @@ query = parse_qs(url.query) page: PageNumber = PageNumber(int(query.get("page", ["0"])[0])) section_list = query.get("section", []) +destination_list = query.get("destination", []) if section_list != []: section: SectionTitle = SectionTitle(section_list[0]) @@ -25,8 +26,16 @@ if section_list != []: else: if len(headers) > 1: notify("", f"Multiple sections '{section}' found: page might be incorrect") - 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)