diff --git a/Makefile b/Makefile index cb05cd2..d76b07f 100644 --- a/Makefile +++ b/Makefile @@ -61,9 +61,6 @@ server-v: .PHONY: build build: dist @$(LEKTOR) build --output-path ../bin --buildstate-path build-state -f ENABLE_PDF_EXPORT - @echo - @echo 'Checking dead links ...' - @python3 extras/find-dead-links.py 'data/development' .PHONY: deploy deploy: @@ -90,6 +87,11 @@ find-links: || echo 'nothing found.' @echo +.PHONY: find-dead-links +find-dead-links: + @echo 'Checking dead links ...' + @python3 extras/find-dead-links.py 'data/development' + .PHONY: find-yield find-yield: @echo diff --git a/src/packages/main/lektor_main/ingredients.py b/src/packages/main/lektor_main/ingredients.py index e3384b5..8b20bd8 100644 --- a/src/packages/main/lektor_main/ingredients.py +++ b/src/packages/main/lektor_main/ingredients.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- from lektor.types import Type -from typing import TYPE_CHECKING, List, Optional, Any +from typing import TYPE_CHECKING, List, Optional, Any, Iterator, Tuple if TYPE_CHECKING: + from lektor.builder import Builder from lektor.db import Record from lektor.types.base import RawValue -from .utils import replaceFractions from .settings import IngredientConfig +from .utils import replaceFractions class IngredientEntry: @@ -111,3 +112,34 @@ class IngredientsListType(Type): def value_from_raw(self, raw: 'RawValue') -> IngredientsDescriptor: return IngredientsDescriptor(raw.value or None) + + +############################## +# Check cross-recipe links # +############################## + +def _detect_atref_urls(recipe: 'Record') -> Iterator[str]: + ''' Internal method to iterate over recipe-links in ingredient notes. ''' + for ing in recipe['ingredients']: + if ing.isIngredient and '@' in ing.note: + for part in ing.note.split(): + if part.startswith('@../'): + yield part[4:].rstrip('/') + + +def check_dead_links(builder: 'Builder') -> Iterator[Tuple['Record', str]]: + ''' + Iterate over all recipes and all ingredients notes. + If a note contains a recipe link, check if the link is a valid target. + If not, print to log but continue building (soft error). + + returns: [recipe, ref-link] + ''' + for alt in builder.pad.config.iter_alternatives(): + # funny enough, .query('/recipes') does not populate ingredients + all_recipes = builder.pad.get('/recipes', alt=alt).children + all_ids = set(x['_slug'] for x in all_recipes) + for recipe in all_recipes: + for ref in _detect_atref_urls(recipe): + if ref not in all_ids: + yield recipe, f'@../{ref}/' diff --git a/src/packages/main/lektor_main/plugin.py b/src/packages/main/lektor_main/plugin.py index d27d6c2..fa80fac 100644 --- a/src/packages/main/lektor_main/plugin.py +++ b/src/packages/main/lektor_main/plugin.py @@ -2,6 +2,7 @@ from lektor.databags import Databags from lektor.db import Page # isinstance from lektor.pluginsystem import Plugin +import os from datetime import datetime from typing import TYPE_CHECKING, Any, List, Dict, Tuple, Set, Iterator if TYPE_CHECKING: @@ -11,8 +12,9 @@ if TYPE_CHECKING: from .durationcluster import ( int_to_cluster, cluster_as_str, human_readable_duration ) -from .ingredients import IngredientsListType +from .ingredients import IngredientsListType, check_dead_links from .latex import TexSources, raw_text_to_tex, html_to_tex +from .log import Log from .settings import Settings from .utils import fillupText, replace_atref_urls, cover_image, noUmlauts @@ -60,6 +62,14 @@ class MainPlugin(Plugin): # or else latex fails because it cannot find referenced images TexSources.build(builder) + def on_after_prune(self, builder: 'Builder', **extra: Any) -> None: + root = self.env.root_path # type: str + with Log.group('check dead links', builder): + for recipe, link in check_dead_links(builder): + Log.error('dead-link: {} ({})'.format( + os.path.relpath(recipe.source_filename or '', root), + Log.Style.red(link))) + ############## # Duration # ##############