Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve links in Markdown text. Support mistune 2.x #992

Merged
merged 12 commits into from Apr 7, 2022
Merged
5 changes: 0 additions & 5 deletions .github/workflows/ci.yml
Expand Up @@ -145,10 +145,5 @@ jobs:
python -m pip install tox tox-gh-actions coverage[toml]
- name: Run python tests
run: tox
- name: Generate coverage.xml
shell: bash
run: |
coverage combine --append || true
coverage xml
- name: Publish coverage data to codecov
uses: codecov/codecov-action@v2
46 changes: 41 additions & 5 deletions lektor/context.py
Expand Up @@ -99,6 +99,7 @@ def __init__(self, artifact=None, pad=None):
self.flow_block_render_stack = []

self._forced_base_url = None
self._resolving_url = False

# General cache system where other things can put their temporary
# stuff in.
Expand Down Expand Up @@ -154,15 +155,30 @@ def base_url(self):
return self.source.url_path
return "/"

def url_to(self, path, alt=None, absolute=None, external=None):
def url_to(
self,
path,
alt=None,
absolute=None,
external=None,
resolve=None,
strict_resolve=None,
):
"""Returns a URL to another path."""
if self.source is None:
raise RuntimeError(
"Can only generate paths to other pages if "
"the context has a source document set."
)
rv = self.source.url_to(path, alt=alt, absolute=True)
return self.pad.make_url(rv, self.base_url, absolute, external)
return self.source.url_to(
path,
alt=alt,
base_url=self.base_url,
absolute=absolute,
external=external,
resolve=resolve,
strict_resolve=strict_resolve,
)

def get_asset_url(self, asset):
"""Calculates the asset URL relative to the current record."""
Expand Down Expand Up @@ -211,8 +227,14 @@ def add_sub_artifact(
self.sub_artifacts.append((aft, build_func))
reporter.report_sub_artifact(aft)

def record_dependency(self, filename):
"""Records a dependency from processing."""
def record_dependency(self, filename, affects_url=None):
"""Records a dependency from processing.

If ``affects_url`` is set to ``False`` the dependency will be ignored if
we are in the process of resolving a URL.
"""
if self._resolving_url and affects_url is False:
return
self.referenced_dependencies.add(filename)
for coll in self._dependency_collectors:
coll(filename)
Expand Down Expand Up @@ -244,3 +266,17 @@ def changed_base_url(self, value):
yield
finally:
self._forced_base_url = old


@contextmanager
def ignore_url_unaffecting_dependencies(value=True):
"""Ignore dependencies which do not affect URL resolution within context."""
ctx = get_ctx()
if ctx is not None:
old = ctx._resolving_url
ctx._resolving_url = value
try:
yield
finally:
if ctx is not None:
ctx._resolving_url = old
17 changes: 16 additions & 1 deletion lektor/db.py
Expand Up @@ -771,6 +771,8 @@ def get_fallback_record_label(self, lang):

def iter_source_filenames(self):
yield self.source_filename
if self.alt != PRIMARY_ALT:
yield self.pad.db.to_fs_path(self["_path"]) + ".lr"
yield self.attachment_filename

@property
Expand Down Expand Up @@ -1525,7 +1527,13 @@ def track_record_dependency(self, record):
ctx = get_ctx()
if ctx is not None:
for filename in record.iter_source_filenames():
ctx.record_dependency(filename)
if isinstance(record, Attachment):
# For Attachments, the actually attachment data
# does not affect the URL of the attachment.
affects_url = filename != record.attachment_filename
else:
affects_url = True
ctx.record_dependency(filename, affects_url=affects_url)
for virtual_source in record.iter_virtual_sources():
ctx.record_virtual_dependency(virtual_source)
if getattr(record, "datamodel", None) and record.datamodel.filename:
Expand Down Expand Up @@ -1625,6 +1633,13 @@ def make_absolute_url(self, url):
def make_url(self, url, base_url=None, absolute=None, external=None):
"""Helper method that creates a finalized URL based on the parameters
provided and the config.

:param url: URL path (starting with "/") relative to the
configured base_path.

:param base_url: Base URL path (starting with "/") relative to
the configured base_path.

"""
url_style = self.db.config.url_style
if absolute is None:
Expand Down
131 changes: 0 additions & 131 deletions lektor/markdown.py

This file was deleted.

106 changes: 106 additions & 0 deletions lektor/markdown/__init__.py
@@ -0,0 +1,106 @@
import sys
from typing import Any
from typing import Dict
from typing import Hashable
from typing import Type
from typing import TYPE_CHECKING
from weakref import ref as weakref

from deprecated import deprecated
from markupsafe import Markup

from lektor.markdown.controller import ControllerCache
from lektor.markdown.controller import FieldOptions
from lektor.markdown.controller import MarkdownController
from lektor.markdown.controller import Meta
from lektor.markdown.controller import RenderResult
from lektor.sourceobj import SourceObject

if sys.version_info >= (3, 8):
from importlib.metadata import version
else:
from importlib_metadata import version

if TYPE_CHECKING: # pragma: no cover
from lektor.environment import Environment


controller_class: Type[MarkdownController]

MISTUNE_VERSION = version("mistune")
if MISTUNE_VERSION.startswith("0."):
from lektor.markdown.mistune0 import MarkdownController0 as controller_class
elif MISTUNE_VERSION.startswith("2."):
from lektor.markdown.mistune2 import MarkdownController2 as controller_class
else: # pragma: no cover
raise ImportError("Unsupported version of mistune")


get_controller = ControllerCache(controller_class)


@deprecated
def make_markdown(env: "Environment") -> Any: # (Environment) -> mistune.Markdown
return get_controller(env).make_parser()


@deprecated
def markdown_to_html(
text: str, record: SourceObject, field_options: FieldOptions
) -> RenderResult:
return get_controller().render(text, record, field_options)


class Markdown:
def __init__(
self, source: str, record: SourceObject, field_options: FieldOptions
) -> None:
self.source = source
self.__record = weakref(record)
self.__field_options = field_options
self.__cache: Dict[Hashable, RenderResult] = {}

def __bool__(self) -> bool:
return bool(self.source)

__nonzero__ = __bool__

@property
def record(self) -> SourceObject:
record = self.__record()
if record is None:
raise RuntimeError("Record has gone away")
return record

def __render(self) -> RenderResult:
# When the markdown instance is attached to a cached object we
# can end up in the situation where, e.g., the base_url has
# changed from the time we were put into the cache to the time
# where we got referenced by something elsewhere. Since this
# affects the processing of relative links, in that case we
# need to re-process our markdown.
controller = get_controller()
key = controller.get_cache_key()
result = self.__cache.get(key) if key is not None else None
if result is None:
result = controller.render(self.source, self.record, self.__field_options)
if key is not None:
self.__cache[key] = result
return result

@property
def meta(self) -> Meta:
return self.__render().meta

@property
def html(self) -> Markup:
return Markup(self.__render().html)

def __getitem__(self, name: str) -> Any:
return self.meta[name]

def __str__(self) -> str:
return self.__render().html

def __html__(self) -> Markup:
return self.html