feature: only external links open in new tab

This commit is contained in:
Daniel Fichtinger 2025-07-11 14:15:03 -04:00
parent b8b8fef72c
commit 0ee8094cc9
3 changed files with 29 additions and 9 deletions

View file

@ -379,6 +379,8 @@ markdown:
enabled: true enabled: true
theme: ashen theme: ashen
wrap: false wrap: false
links:
external_new_tab: true
blog: blog:
dir: blog dir: blog
``` ```
@ -391,6 +393,7 @@ blog:
| `markdown.syntax_highlighting.enabled` | Whether code should be highlighted. | | `markdown.syntax_highlighting.enabled` | Whether code should be highlighted. |
| `markdown.syntax_highlighting.theme` | [Pygments] style for highlighting. | | `markdown.syntax_highlighting.theme` | [Pygments] style for highlighting. |
| `markdown.syntax_highlighting.wrap` | Whether the resulting code block should be word wrapped. | | `markdown.syntax_highlighting.wrap` | Whether the resulting code block should be word wrapped. |
| `markdown.links.external_new_tab` | Whether external links should be opened in a new tab. |
| `blog.dir` | Name of a directory relative to `content/` whose children are automatically considered posts. | | `blog.dir` | Name of a directory relative to `content/` whose children are automatically considered posts. |
### Sitemap ### Sitemap

View file

@ -37,6 +37,11 @@ class HighlightingConfig:
wrap: bool = False wrap: bool = False
@dataclass
class LinksConfig:
external_new_tab: bool = True
@dataclass @dataclass
class MarkdownConfig: class MarkdownConfig:
image_labels: bool = True image_labels: bool = True
@ -44,6 +49,7 @@ class MarkdownConfig:
syntax_highlighting: HighlightingConfig = field( syntax_highlighting: HighlightingConfig = field(
default_factory=HighlightingConfig default_factory=HighlightingConfig
) )
links: LinksConfig = field(default_factory=LinksConfig)
@dataclass @dataclass

View file

@ -1,11 +1,10 @@
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
from collections.abc import Sequence from collections.abc import Sequence
from logging import Logger
from pathlib import Path from pathlib import Path
from typing import Any, override from typing import Any, override
from l2m4m import LaTeX2MathMLExtension from l2m4m import LaTeX2MathMLExtension
# from l2m4m import LaTeX2MathMLExtension
from markdown import Markdown from markdown import Markdown
from markdown.extensions.abbr import AbbrExtension from markdown.extensions.abbr import AbbrExtension
from markdown.extensions.attr_list import AttrListExtension from markdown.extensions.attr_list import AttrListExtension
@ -34,8 +33,6 @@ from zona.log import get_logger
from zona.metadata import Metadata from zona.metadata import Metadata
from zona.models import Item from zona.models import Item
logger = get_logger()
class ZonaImageTreeprocessor(Treeprocessor): class ZonaImageTreeprocessor(Treeprocessor):
"""Implement Zona's image caption rendering.""" """Implement Zona's image caption rendering."""
@ -43,6 +40,7 @@ class ZonaImageTreeprocessor(Treeprocessor):
def __init__(self, md: Markdown): def __init__(self, md: Markdown):
super().__init__() super().__init__()
self.md: Markdown = md self.md: Markdown = md
self.logger: Logger = get_logger()
@override @override
def run(self, root: etree.Element): def run(self, root: etree.Element):
@ -87,6 +85,7 @@ class ZonaLinkTreeprocessor(Treeprocessor):
): ):
super().__init__() super().__init__()
self.resolve: bool = resolve self.resolve: bool = resolve
self.logger: Logger = get_logger()
if self.resolve: if self.resolve:
assert source is not None assert source is not None
assert layout is not None assert layout is not None
@ -103,9 +102,15 @@ class ZonaLinkTreeprocessor(Treeprocessor):
if not href: if not href:
continue continue
if self.resolve: if self.resolve:
assert self.config
cur = Path(href) cur = Path(href)
_href = href _href = href
if href.startswith("/"): same_file = False
resolved = Path()
# href starting with anchor reference the current file
if href.startswith("#"):
same_file = True
elif href.startswith("/"):
# resolve relative to content root # resolve relative to content root
resolved = ( resolved = (
self.layout.content / cur.relative_to("/") self.layout.content / cur.relative_to("/")
@ -114,19 +119,25 @@ class ZonaLinkTreeprocessor(Treeprocessor):
# treat as relative link and try to resolve # treat as relative link and try to resolve
resolved = (self.source.parent / cur).resolve() resolved = (self.source.parent / cur).resolve()
# only substitute if link points to an actual file # only substitute if link points to an actual file
if resolved.exists(): # that isn't the self file
if not same_file and resolved.exists():
item = self.item_map.get(resolved) item = self.item_map.get(resolved)
if item: if item:
href = util.normalize_url(item.url) href = util.normalize_url(item.url)
element.set("href", href) element.set("href", href)
logger.debug( self.logger.debug(
f"Link in file {self.source}: {_href} resolved to {href}" f"Link in file {self.source}: {_href} resolved to {href}"
) )
else: else:
logger.debug( self.logger.debug(
f"Warning: resolved path {resolved} not found in item map" f"Warning: resolved path {resolved} not found in item map"
) )
element.set("target", "_blank") # open link in new tab if not self-link
elif (
self.config.markdown.links.external_new_tab
and not same_file
):
element.set("target", "_blank")
def get_formatter(config: ZonaConfig): def get_formatter(config: ZonaConfig):