diff --git a/.dockerignore b/.dockerignore index 0a4bca2c..8906c6fc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,6 +11,9 @@ venv/ **/*.db **/repo **/data +**/data2 +**/data-indexing +**/data-new-ctags .git .gitignore diff --git a/README.adoc b/README.adoc index e071592b..44122e4d 100644 --- a/README.adoc +++ b/README.adoc @@ -364,6 +364,8 @@ new project: ln -s /srv/git/zephyr.git repo export LXR_DATA_DIR=$LXR_PROJ_DIR/data export LXR_REPO_DIR=$LXR_PROJ_DIR/repo + export LXR_DATA_DIR=$LXR_PROJ_DIR/data + export LXR_REPO_DIR=$LXR_PROJ_DIR/repo Now, go back to the Elixir sources and test that tags are correctly extracted: @@ -371,9 +373,6 @@ extracted: ./script.sh list-tags Depending on how you want to show the available versions on the Elixir pages, -you may have to apply substitutions to each tag string, for example to add -a `v` prefix if missing, for consistency with how other project versions are -shown. You may also decide to ignore specific tags. All this can be done by redefining the default `list_tags()` function in a new `projects/<projectname>.sh` file. Here's an example (`projects/zephyr.sh` file): @@ -429,8 +428,6 @@ You can then check that Elixir works through your http server. == Coding style -If you wish to contribute to Elixir's Python code, please -follow the https://www.python.org/dev/peps/pep-0008/[official coding style for Python]. == How to send patches diff --git a/docker/Dockerfile b/docker/Dockerfile index 17fbc340..d1a54e70 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -65,6 +65,15 @@ ENV ELIXIR_VERSION=$ELIXIR_VERSION \ PATH="/usr/local/elixir/utils:/usr/local/elixir/venv/bin:$PATH" \ PYTHONUNBUFFERED=1 -COPY . /usr/local/elixir/ +COPY ./elixir/ /usr/local/elixir +COPY ./utils/ /usr/local/elixir +COPY ./projects/ /usr/local/elixir +COPY ./templates/ /usr/local/elixir +COPY ./static/ /usr/local/elixir +COPY ./utils/ /usr/local/elixir +COPY ./t/ /usr/local/elixir +COPY samples/ /usr/local/elixir +COPY *.py /usr/local/elixir +COPY *.sh /usr/local/elixir ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"] diff --git a/elixir/filters/configin.py b/elixir/filters/configin.py index fa765a2d..f6967454 100755 --- a/elixir/filters/configin.py +++ b/elixir/filters/configin.py @@ -1,5 +1,5 @@ import re -from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches +from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches, format_source_link # Filters for Config.in includes # source "path/file" @@ -23,7 +23,7 @@ def keep_configin(m): def untransform_formatted_code(self, ctx: FilterContext, html: str) -> str: def replace_configin(m): w = self.configin[decode_number(m.group(1)) - 1] - return f'<a href="{ ctx.get_absolute_source_url(w) }">{ w }</a>' + return format_source_link(ctx.get_absolute_source_url(w), w) return re.sub('__KEEPCONFIGIN__([A-J]+)', replace_configin, html, flags=re.MULTILINE) diff --git a/elixir/filters/cppinc.py b/elixir/filters/cppinc.py index c4ffab9d..517f3ef3 100755 --- a/elixir/filters/cppinc.py +++ b/elixir/filters/cppinc.py @@ -1,5 +1,5 @@ import re -from .utils import Filter, FilterContext, encode_number, decode_number, extension_matches +from .utils import Filter, FilterContext, encode_number, decode_number, extension_matches, format_source_link # Filters for cpp includes like these: # #include "file" @@ -24,8 +24,7 @@ def keep_cppinc(m): def untransform_formatted_code(self, ctx: FilterContext, html: str) -> str: def replace_cppinc(m): w = self.cppinc[decode_number(m.group(1)) - 1] - url = ctx.get_relative_source_url(w) - return f'<a href="{ url }">{ w }</a>' + return format_source_link(ctx.get_relative_source_url(w), w) return re.sub('__KEEPCPPINC__([A-J]+)', replace_cppinc, html, flags=re.MULTILINE) diff --git a/elixir/filters/cpppathinc.py b/elixir/filters/cpppathinc.py index 6d3c48c2..964989be 100755 --- a/elixir/filters/cpppathinc.py +++ b/elixir/filters/cpppathinc.py @@ -1,5 +1,5 @@ import re -from .utils import Filter, FilterContext, encode_number, decode_number, extension_matches +from .utils import Filter, FilterContext, encode_number, decode_number, extension_matches, format_source_link # Filters for cpp includes like these: # #include <file> @@ -36,7 +36,7 @@ def untransform_formatted_code(self, ctx: FilterContext, html: str) -> str: def replace_cpppathinc(m): w = self.cpppathinc[decode_number(m.group(1)) - 1] path = f'/include/{ w }' - return f'<a href="{ ctx.get_absolute_source_url(path) }">{ w }</a>' + return format_source_link(ctx.get_absolute_source_url(path), w) return re.sub('__KEEPCPPPATHINC__([A-J]+)', replace_cpppathinc, html, flags=re.MULTILINE) diff --git a/elixir/filters/dtsi.py b/elixir/filters/dtsi.py index 1ef3fba0..44399a54 100755 --- a/elixir/filters/dtsi.py +++ b/elixir/filters/dtsi.py @@ -1,5 +1,5 @@ import re -from .utils import Filter, FilterContext, encode_number, decode_number, extension_matches +from .utils import Filter, FilterContext, encode_number, decode_number, extension_matches, format_source_link # Filters for dts includes as follows: # Replaces include directives in dts/dtsi files with links to source @@ -24,7 +24,7 @@ def keep_dtsi(m): def untransform_formatted_code(self, ctx: FilterContext, html: str) -> str: def replace_dtsi(m): w = self.dtsi[decode_number(m.group(1)) - 1] - return f'<a href="{ ctx.get_relative_source_url(w) }">{ w }</a>' + return format_source_link(ctx.get_relative_source_url(w), w) return re.sub('__KEEPDTSI__([A-J]+)', replace_dtsi, html, flags=re.MULTILINE) diff --git a/elixir/filters/kconfig.py b/elixir/filters/kconfig.py index f21dee37..710700c4 100755 --- a/elixir/filters/kconfig.py +++ b/elixir/filters/kconfig.py @@ -1,5 +1,5 @@ import re -from .utils import Filter, FilterContext, encode_number, decode_number, filename_without_ext_matches +from .utils import Filter, FilterContext, encode_number, decode_number, filename_without_ext_matches, format_source_link # Filters for Kconfig includes # Replaces KConfig includes (source keyword) with links to included files @@ -24,7 +24,7 @@ def keep_kconfig(m): def untransform_formatted_code(self, ctx: FilterContext, html: str) -> str: def replace_kconfig(m): w = self.kconfig[decode_number(m.group(1)) - 1] - return f'<a href="{ ctx.get_absolute_source_url(w) }">{ w }</a>' + return format_source_link(ctx.get_absolute_source_url(w), w) return re.sub('__KEEPKCONFIG__([A-J]+)', replace_kconfig, html, flags=re.MULTILINE) diff --git a/elixir/filters/makefiledir.py b/elixir/filters/makefiledir.py index 73015c73..fc786c6c 100755 --- a/elixir/filters/makefiledir.py +++ b/elixir/filters/makefiledir.py @@ -1,6 +1,6 @@ from os.path import dirname import re -from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches +from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches, format_source_link # Filters for Makefile directory includes as follows: # obj-$(VALUE) += dir/ @@ -39,7 +39,7 @@ def replace_makefiledir(m): fpath = f'{ filedir }{ w }/Makefile' - return f'<a href="{ ctx.get_absolute_source_url(fpath) }">{ w }/</a>' + return format_source_link(ctx.get_absolute_source_url(fpath), w) return re.sub('__KEEPMAKEFILEDIR__([A-J]+)/', replace_makefiledir, html, flags=re.MULTILINE) diff --git a/elixir/filters/makefiledtb.py b/elixir/filters/makefiledtb.py index a7fa4163..0b4f4eef 100755 --- a/elixir/filters/makefiledtb.py +++ b/elixir/filters/makefiledtb.py @@ -1,6 +1,6 @@ from os.path import dirname import re -from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches +from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches, format_source_link # Filters for Makefile file includes like these: # dtb-y += file.dtb @@ -30,7 +30,7 @@ def replace_makefiledtb(m): filedir += '/' npath = f'{ filedir }{ w }.dts' - return f'<a href="{ ctx.get_absolute_source_url(npath) }">{ w }.dtb</a>' + return format_source_link(ctx.get_absolute_source_url(npath), w+'.dtb') return re.sub('__KEEPMAKEFILEDTB__([A-J]+)\.dtb', replace_makefiledtb, html, flags=re.MULTILINE) diff --git a/elixir/filters/makefilefile.py b/elixir/filters/makefilefile.py index be60bef5..09a31873 100755 --- a/elixir/filters/makefilefile.py +++ b/elixir/filters/makefilefile.py @@ -1,6 +1,6 @@ from os.path import dirname import re -from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches +from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches, format_source_link # Filters for files listed in Makefiles # path/file @@ -38,7 +38,7 @@ def replace_makefilefile(m): filedir += '/' npath = filedir + w - return f'<a href="{ ctx.get_absolute_source_url(npath) }">{ w }</a>' + return format_source_link(ctx.get_absolute_source_url(npath), w) return re.sub('__KEEPMAKEFILEFILE__([A-J]+)', replace_makefilefile, html, flags=re.MULTILINE) diff --git a/elixir/filters/makefileo.py b/elixir/filters/makefileo.py index 6c8f0fe9..e332b31d 100755 --- a/elixir/filters/makefileo.py +++ b/elixir/filters/makefileo.py @@ -1,6 +1,6 @@ from os.path import dirname import re -from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches +from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches, format_source_link # Filters for Makefile file includes like these: # file.o @@ -30,7 +30,7 @@ def replace_makefileo(m): filedir += '/' npath = f'{ filedir }{ w }.c' - return f'<a href="{ ctx.get_absolute_source_url(npath) }">{ w }.o</a>' + return format_source_link(ctx.get_absolute_source_url(npath), w+'.o') return re.sub('__KEEPMAKEFILEO__([A-J]+)\.o', replace_makefileo, html, flags=re.MULTILINE) diff --git a/elixir/filters/makefilesrctree.py b/elixir/filters/makefilesrctree.py index 4149439f..25531deb 100755 --- a/elixir/filters/makefilesrctree.py +++ b/elixir/filters/makefilesrctree.py @@ -1,5 +1,5 @@ import re -from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches +from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches, format_source_link # Filters for files listed in Makefiles using $(srctree) # $(srctree)/Makefile @@ -27,8 +27,7 @@ def keep_makefilesrctree(m): def untransform_formatted_code(self, ctx: FilterContext, html: str) -> str: def replace_makefilesrctree(m): w = self.makefilesrctree[decode_number(m.group(1)) - 1] - url = ctx.get_absolute_source_url(w) - return f'<a href="{ url }">$(srctree)/{ w }</a>' + return format_source_link(ctx.get_absolute_source_url(w), f'$(srctree)/{ w }') return re.sub('__KEEPMAKEFILESRCTREE__([A-J]+)', replace_makefilesrctree, html, flags=re.MULTILINE) diff --git a/elixir/filters/makefilesubdir.py b/elixir/filters/makefilesubdir.py index e4c6777d..1a097c50 100755 --- a/elixir/filters/makefilesubdir.py +++ b/elixir/filters/makefilesubdir.py @@ -1,6 +1,6 @@ from os.path import dirname import re -from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches +from .utils import Filter, FilterContext, decode_number, encode_number, filename_without_ext_matches, format_source_link # Filters for Makefile directory includes as follows: # subdir-y += dir @@ -31,7 +31,7 @@ def replace_makefilesubdir(m): filedir += '/' npath = f'{ filedir }{ w }/Makefile' - return f'<a href="{ ctx.get_absolute_source_url(npath) }">{ w }</a>' + return format_source_link(ctx.get_absolute_source_url(npath), w) return re.sub('__KEEPMAKESUBDIR__([A-J]+)', replace_makefilesubdir, html, flags=re.MULTILINE) diff --git a/elixir/filters/utils.py b/elixir/filters/utils.py index e0d74398..43d7e4a1 100755 --- a/elixir/filters/utils.py +++ b/elixir/filters/utils.py @@ -91,3 +91,6 @@ def decode_number(string): return int(result) +def format_source_link(url: str, label: str) -> str: + return f'<a class="source-link" href="{ url }">{ label }</a>' + diff --git a/elixir/query.py b/elixir/query.py index 1e740bf2..97313d3b 100755 --- a/elixir/query.py +++ b/elixir/query.py @@ -22,6 +22,7 @@ from . import lib from . import data import os +import sys from collections import OrderedDict from urllib import parse @@ -175,6 +176,9 @@ def get_versions(self): def get_file_type(self, version, path): return decode(self.script('get-type', version, path)).strip() + def get_blob_id(self, version, path): + return decode(self.script('get-blob-id', version, path)).strip() + # Returns identifier search results def search_ident(self, version, ident, family): # DT bindings compatible strings are handled differently @@ -195,6 +199,25 @@ def get_latest_tag(self): # return the oldest tag, even if it does not exist in the database return sorted_tags[-1].decode() + def get_diff(self, version, version_other, path): + data = decode(self.script('get-diff', version, version_other, path)).split('\n') + result = [] + for line in data: + if len(line) == 0: + continue + elif line[0] == '+': + line_num_left, line_num_right, changes = line[1:].split(':') + result.append(('+', int(line_num_left), int(line_num_right), int(changes))) + elif line[0] == '-': + line_num_left, line_num_right, changes = line[1:].split(':') + result.append(('-', int(line_num_left), int(line_num_right), int(changes))) + elif line[0] == '=': + line_num, changes, other_line_num, other_changes = line[1:].split(':') + result.append(('=', int(line_num), int(changes), int(other_line_num), int(other_changes))) + else: + raise Exception("Invalid line in get-diff: " + line) + return result + def get_file_raw(self, version, path): return decode(self.script('get-file', version, path)) diff --git a/elixir/web.py b/elixir/web.py index 23b1265c..6dfce2ff 100755 --- a/elixir/web.py +++ b/elixir/web.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 + # This file is part of Elixir, a source code cross-referencer. # # Copyright (C) 2017--2020 Mikaƫl Bouillot <mikael.bouillot@bootlin.com> @@ -25,9 +26,11 @@ import threading import time import datetime +import dataclasses from collections import OrderedDict, namedtuple from re import search, sub -from typing import Any, Callable, NamedTuple, Tuple +from typing import Any, Callable, NamedTuple, Optional, Tuple +from difflib import SequenceMatcher from urllib import parse import falcon import jinja2 @@ -40,7 +43,7 @@ from .api import ApiIdentGetterResource from .query import get_query from .web_utils import ProjectConverter, IdentConverter, validate_version, validate_project, validate_ident, \ - get_elixir_version_string, get_elixir_repo_url, RequestContext, Config + get_elixir_version_string, get_elixir_repo_url, RequestContext, Config, DiffFormater VERSION_CACHE_DURATION_SECONDS = 2 * 60 # 2 minutes ADD_ISSUE_LINK = "https://github.com/bootlin/elixir/issues/new" @@ -108,7 +111,7 @@ def get_project_error_page(req, resp, exception: ElixirProjectError): versions_raw = get_versions_cached(query, req.context, project) get_url_with_new_version = lambda v: stringify_source_path(project, v, '/') - versions, current_version_path = get_versions(versions_raw, get_url_with_new_version, version) + versions, current_version_path = get_versions(versions_raw, get_url_with_new_version, None, version) if current_version_path[2] is None: # If details about current version are not available, make base links @@ -192,6 +195,10 @@ def validate_project_and_version(ctx, project, version): def get_source_base_url(project: str, version: str) -> str: return f'/{ parse.quote(project, safe="") }/{ parse.quote(version, safe="") }/source' +def stringify_diff_path(project: str, version: str, version_other: str, path: str) -> str: + return f'/{ parse.quote(project, safe="") }/{ parse.quote(version, safe="") }/diff/' + \ + f'{ parse.quote(version_other, safe="") }/{ path }' + # Converts ParsedSourcePath to a string with corresponding URL path def stringify_source_path(project: str, version: str, path: str) -> str: if not path.startswith('/'): @@ -249,12 +256,48 @@ def on_get(self, req, resp, project: str, version: str, path: str): query.close() +# Returns base url of diff pages +# project and version are assumed to be unquoted +def get_diff_base_url(project: str, version: str, version_other: str) -> str: + return f'/{ parse.quote(project, safe="") }/{ parse.quote(version, safe="") }/diff/{ parse.quote(version_other, safe="") }' + # Handles source URLs without a path, ex. '/u-boot/v2023.10/source'. # Note lack of trailing slash class SourceWithoutPathResource(SourceResource): def on_get(self, req, resp, project: str, version: str): return super().on_get(req, resp, project, version, '') +class DiffResource: + def on_get(self, req, resp, project: str, version: str, version_other: str, path: str = ''): + project, version, query = validate_project_and_version(req.context, project, version) + version_other = validate_version(parse.unquote(version_other)) + if version_other is None or version_other == 'latest': + raise ElixirProjectError('Error', 'Invalid other version', project=project, query=query) + + if not path.startswith('/') and len(path) != 0: + path = f'/{ path }' + + if path.endswith('/'): + resp.status = falcon.HTTP_MOVED_PERMANENTLY + resp.location = stringify_source_path(project, version, path) + return + + # Check if path contains only allowed characters + if not search('^[A-Za-z0-9_/.,+-]*$', path): + raise ElixirProjectError('Error', 'Path contains characters that are not allowed', + project=project, version=version, query=query) + + if version == 'latest': + version = parse.quote(query.get_latest_tag()) + resp.status = falcon.HTTP_FOUND + resp.location = stringify_source_path(project, version, path) + return + + resp.content_type = falcon.MEDIA_HTML + resp.status, resp.text = generate_diff_page(req.context, query, project, version, version_other, path) + + query.close() + # Returns base url of ident pages # project and version assumed unquoted @@ -399,7 +442,9 @@ def get_projects(basedir: str) -> list[ProjectEntry]: # Tuple of version name and URL to chosen resource with that version # Used to render version list in the sidebar -VersionEntry = namedtuple('VersionEntry', 'version, url') +VersionEntry = namedtuple('VersionEntry', 'version, url, diff_url') + +VersionPath = namedtuple('VersionPath', 'major, minor, path') # Takes result of Query.get_versions() and prepares it for the sidebar template. # Returns an OrderedDict with version information and optionally a triple with @@ -412,10 +457,11 @@ def get_projects(basedir: str) -> list[ProjectEntry]: # current_version: string with currently browsed version def get_versions(versions: OrderedDict[str, OrderedDict[str, str]], get_url: Callable[[str], str], - current_version: str) -> Tuple[dict[str, dict[str, list[VersionEntry]]], Tuple[str|None, str|None, str|None]]: + get_diff_url: Optional[Callable[[str], str]], + current_version: str) -> Tuple[dict[str, dict[str, list[VersionEntry]]], VersionPath]: result = OrderedDict() - current_version_path = (None, None, None) + current_version_path = VersionPath(None, None, None) for major, minor_verions in versions.items(): for minor, patch_versions in minor_verions.items(): for v in patch_versions: @@ -423,13 +469,24 @@ def get_versions(versions: OrderedDict[str, OrderedDict[str, str]], result[major] = OrderedDict() if minor not in result[major]: result[major][minor] = [] - result[major][minor].append(VersionEntry(v, get_url(v))) + result[major][minor].append( + VersionEntry(v, get_url(v), get_diff_url(v) if get_diff_url is not None else None) + ) if v == current_version: - current_version_path = (major, minor, v) + current_version_path = VersionPath(major, minor, v) return result, current_version_path -# Caches get_versions result in a context object +def find_version_path(versions: OrderedDict[str, OrderedDict[str, str]], + version: str) -> VersionPath: + for major, minor_verions in versions.items(): + for minor, patch_versions in minor_verions.items(): + for v in patch_versions: + if v == version: + return VersionPath(major, minor, v) + + return VersionPath(None, None, None) + def get_versions_cached(q, ctx, project): with ctx.versions_cache_lock: if project not in ctx.versions_cache: @@ -448,14 +505,15 @@ def get_versions_cached(q, ctx, project): # project: name of the project # version: version of the project def get_layout_template_context(q: Query, ctx: RequestContext, get_url_with_new_version: Callable[[str], str], - project: str, version: str) -> dict[str, Any]: + get_diff_url: Optional[Callable[[str], str]], project: str, version: str) -> dict[str, Any]: versions_raw = get_versions_cached(q, ctx, project) - versions, current_version_path = get_versions(versions_raw, get_url_with_new_version, version) + versions, current_version_path = get_versions(versions_raw, get_url_with_new_version, get_diff_url, version) return { 'projects': get_projects(ctx.config.project_dir), 'versions': versions, 'current_version_path': current_version_path, + 'other_version_path': (None, None, None), 'topbar_families': TOPBAR_FAMILIES, 'elixir_version_string': ctx.config.version_string, 'elixir_repo_url': ctx.config.repo_url, @@ -501,7 +559,7 @@ def format_code(filename: str, code: str) -> str: lexer.stripnl = False formatter = pygments.formatters.HtmlFormatter( # Adds line numbers column to output - linenos=True, + linenos='inline', # Wraps line numbers in link (a) tags anchorlinenos=True, # Wraps each line in a span tag with id='codeline-{line_number}' @@ -548,20 +606,121 @@ def get_ident_url(ident, ident_family=None): html_code_block = format_code(fname, code) # Replace line numbers by links to the corresponding line in the current file - html_code_block = sub('href="#codeline-(\d+)', 'name="L\\1" id="L\\1" href="#L\\1', html_code_block) + html_code_block = sub('href="#codeline-(\d+)', 'class="line-link" name="L\\1" id="L\\1" href="#L\\1', html_code_block) for f in filters: html_code_block = f.untransform_formatted_code(filter_ctx, html_code_block) return html_code_block +def format_diff(filename: str, diff, code: str, code_other: str) -> Tuple[str, str]: + import pygments + import pygments.lexers + import pygments.formatters + from pygments.lexers.asm import GasLexer + from pygments.lexers.r import SLexer + + try: + lexer = pygments.lexers.guess_lexer_for_filename(filename, code) + if filename.endswith('.S') and isinstance(lexer, SLexer): + lexer = GasLexer() + except pygments.util.ClassNotFound: + lexer = pygments.lexers.get_lexer_by_name('text') + + lexer.stripnl = False + + formatter = DiffFormater( + diff, + True, + # Adds line numbers column to output + linenos='inline', + # Wraps line numbers in link (a) tags + anchorlinenos=True, + # Wraps each line in a span tag with id='codeline-{line_number}' + linespans='codeline', + ) + + formatter_other = DiffFormater( + diff, + False, + # Adds line numbers column to output + linenos='inline', + # Wraps line numbers in link (a) tags + anchorlinenos=True, + # Wraps each line in a span tag with id='codeline-{line_number}' + linespans='codeline', + ) + + return pygments.highlight(code, lexer, formatter), pygments.highlight(code_other, lexer, formatter_other) + +def generate_diff(q: Query, project: str, version: str, version_other: str, path: str) -> Tuple[str, str]: + code = q.get_tokenized_file(version, path) + code_other = q.get_tokenized_file(version_other, path) + diff = q.get_diff(version, version_other, path) + + _, fname = os.path.split(path) + _, extension = os.path.splitext(fname) + extension = extension[1:].lower() + family = getFileFamily(fname) + + source_base_url = get_source_base_url(project, version) + source_base_url_other = get_source_base_url(project, version_other) + + def generate_get_ident_url(version): + def get_ident_url(ident, ident_family=None): + if ident_family is None: + ident_family = family + return stringify_ident_path(project, version, ident_family, ident) + return get_ident_url + + filter_ctx = FilterContext( + q, + version, + family, + path, + generate_get_ident_url(version), + lambda path: f'{ source_base_url }{ "/" if not path.startswith("/") else "" }{ path }', + lambda rel_path: f'{ source_base_url }{ os.path.dirname(path) }/{ rel_path }', + ) + + filter_ctx_other = dataclasses.replace(filter_ctx, + tag=version_other, + get_ident_url=generate_get_ident_url(version_other), + get_absolute_source_url=lambda path: f'{ source_base_url_other }{ "/" if not path.startswith("/") else "" }{ path }', + get_relative_source_url=lambda rel_path: f'{ source_base_url_other }{ os.path.dirname(path) }/{ rel_path }', + ) + + filters = get_filters(filter_ctx, project) + filters_other = get_filters(filter_ctx_other, project) + + # Apply filters + for f in filters: + code = f.transform_raw_code(filter_ctx, code) + for f in filters_other: + code_other = f.transform_raw_code(filter_ctx_other, code_other) + + html_code_block, html_code_other_block = format_diff(fname, diff, code, code_other) + + # Replace line numbers by links to the corresponding line in the current file + html_code_block = sub('href="#codeline-(\d+)', + 'class="line-link" name="L\\1" id="L\\1" href="#L\\1', html_code_block) + html_code_other_block = sub('href="#codeline-(\d+)', + 'class="line-link" name="OL\\1" id="OL\\1" href="#OL\\1', html_code_other_block) + + for f in filters: + html_code_block = f.untransform_formatted_code(filter_ctx, html_code_block) + for f in filters_other: + html_code_other_block = f.untransform_formatted_code(filter_ctx_other, html_code_other_block) + + return html_code_block, html_code_other_block + # Represents a file entry in git tree # type : either tree (directory), blob (file) or symlink # name: filename of the file # path: path of the file, path to the target in case of symlinks # url: absolute URL of the file # size: int, file size in bytes, None for directories and symlinks -DirectoryEntry = namedtuple('DirectoryEntry', 'type, name, path, url, size') +DirectoryEntry = namedtuple('DirectoryEntry', 'type, name, path, url, size, cls') # Returns a list of DirectoryEntry objects with information about files in a directory # base_url: file URLs will be created by appending file path to this URL. It shouldn't end with a slash @@ -572,11 +731,11 @@ def get_directory_entries(q: Query, base_url, tag: str, path: str) -> list[Direc lines = q.get_dir_contents(tag, path) for l in lines: - type, name, size, perm = l.split(' ') + type, name, size, perm, blob_id = l.split(' ') file_path = f"{ path }/{ name }" if type == 'tree': - dir_entries.append(DirectoryEntry('tree', name, file_path, f"{ base_url }{ file_path }", None)) + dir_entries.append(DirectoryEntry('tree', name, file_path, f"{ base_url }{ file_path }", None, None)) elif type == 'blob': # 120000 permission means it's a symlink if perm == '120000': @@ -584,9 +743,9 @@ def get_directory_entries(q: Query, base_url, tag: str, path: str) -> list[Direc link_contents = q.get_file_raw(tag, file_path) link_target_path = os.path.abspath(dir_path + link_contents) - dir_entries.append(DirectoryEntry('symlink', name, link_target_path, f"{ base_url }{ link_target_path }", size)) + dir_entries.append(DirectoryEntry('symlink', name, link_target_path, f"{ base_url }{ link_target_path }", size, None)) else: - dir_entries.append(DirectoryEntry('blob', name, file_path, f"{ base_url }{ file_path }", size)) + dir_entries.append(DirectoryEntry('blob', name, file_path, f"{ base_url }{ file_path }", size, None)) return dir_entries @@ -641,20 +800,185 @@ def generate_source_page(ctx: RequestContext, q: Query, title_path = f'{ path_split[-1] } - { "/".join(path_split) } - ' get_url_with_new_version = lambda v: stringify_source_path(project, v, path) + get_diff_url = lambda v_other: stringify_diff_path(project, version, v_other, path) # Create template context data = { - **get_layout_template_context(q, ctx, get_url_with_new_version, project, version), + **get_layout_template_context(q, ctx, get_url_with_new_version, get_diff_url, project, version), 'title_path': title_path, 'path': path, 'breadcrumb_urls': breadcrumb_urls, + 'diff_mode_available': True, **template_ctx, } return (status, template.render(data)) +# Returns a list of DirectoryEntry objects with information about files in a directory +# base_url: file URLs will be created by appending file path to this URL. It shouldn't end with a slash +# tag: requested repository tag +# tag_other: tag to diff with +# path: path to the directory in the repository +def diff_directory_entries(q: Query, base_url, tag: str, tag_other: str, path: str) -> list[DirectoryEntry]: + dir_entries = [] + + names, names_other = {}, {} + for line in q.get_dir_contents(tag, path): + n = line.split(' ') + names[n[1]] = n + for line in q.get_dir_contents(tag_other, path): + n = line.split(' ') + names_other[n[1]] = n + + def dir_sort(name): + if name in names and names[name][0] == 'tree': + return (1, name) + elif name in names_other and names_other[name][0] == 'tree': + return (1, name) + else: + return (2, name) + + all_names = set(names.keys()) + all_names = all_names.union(names_other.keys()) + all_names = sorted(all_names, key=dir_sort) + + for name in all_names: + data = names.get(name) + data_other = names_other.get(name) + + cls = None + if data is None and data_other is not None: + type, name, size, perm, blob_id = data_other + cls = 'added' + elif data_other is None and data is not None: + type, name, size, perm, blob_id = data + cls = 'removed' + elif data is not None and data_other is not None: + type_old, name, _, _, blob_id = data + type, _, size, perm, blob_id_other = data_other + if blob_id != blob_id_other or type_old != type: + cls = 'changed' + else: + raise Exception("name does not exist " + name) + + file_path = f"{ path }/{ name }" + + if type == 'tree': + dir_entries.append(DirectoryEntry('tree', name, file_path, f"{ base_url }{ file_path }", None, cls)) + elif type == 'blob': + # 120000 permission means it's a symlink + if perm == '120000': + dir_path = path if path.endswith('/') else path + '/' + link_contents = q.get_file_raw(tag, file_path) + link_target_path = os.path.abspath(dir_path + link_contents) + + dir_entries.append(DirectoryEntry('symlink', name, link_target_path, f"{ base_url }{ link_target_path }", size, cls)) + else: + dir_entries.append(DirectoryEntry('blob', name, file_path, f"{ base_url }{ file_path }", size, cls)) + + return dir_entries + +# Generates response (status code and optionally HTML) of the `diff` route +def generate_diff_page(ctx: RequestContext, q: Query, + project: str, version: str, version_other: str, path: str) -> tuple[int, str]: + + status = falcon.HTTP_OK + diff_base_url = get_diff_base_url(project, version, version_other) + + # Generate breadcrumbs + path_split = path.split('/')[1:] + path_temp = '' + breadcrumb_links = [] + for p in path_split: + path_temp += '/'+p + breadcrumb_links.append((p, f'{ diff_base_url }{ path_temp }')) + + type = q.get_file_type(version, path) + type_other = q.get_file_type(version_other, path) + blob_id = q.get_blob_id(version, path) + blob_id_other = q.get_blob_id(version_other, path) + + if type == 'tree' or type_other == 'tree': + back_path = os.path.dirname(path[:-1]) + if back_path == '/': + back_path = '' + + def generate_warning(type, version): + if type == 'blob': + return f'{path} is a file in {version}' + elif type == '': + return f'{path} does not exist in {version}' + + warnings = [generate_warning(type, version), generate_warning(type_other, version)] + + template_ctx = { + 'dir_entries': diff_directory_entries(q, diff_base_url, version, version_other, path), + 'back_url': f'{ diff_base_url }{ back_path }' if path != '' else None, + 'warnings': warnings + } + template = ctx.jinja_env.get_template('tree.html') + elif type == 'blob' or type_other == 'blob': + if type == type_other == 'blob' and blob_id != blob_id_other: + code, code_other = generate_diff(q, project, version, version_other, path) + template_ctx = { + 'code': code, + 'code_other': code_other, + 'other_tag': parse.unquote(version_other), + } + template = ctx.jinja_env.get_template('diff.html') + else: + if blob_id == blob_id_other: + warning = f'Files are the same in {version} and {version_other}.' + else: + missing_version = version_other if type == 'blob' else version + shown_version = version if type == 'blob' else version_other + warning = f'File does not exist, or is not a file in {missing_version}. ({shown_version} displayed)' + + template_ctx = { + 'code': generate_source(q, project, version if type == 'blob' else version_other, path), + 'warning': warning + } + template = ctx.jinja_env.get_template('source.html') + else: + raise ElixirProjectError('File not found', f'This file does not exist in {version} nor in {version_other}.', + status=falcon.HTTP_NOT_FOUND, + query=q, project=project, version=version, + extra_template_args={'breadcrumb_links': breadcrumb_links}) + + # Create titles like this: + # root path: "Linux source code (v5.5.6) - Bootlin" + # first level path: "arch - Linux source code (v5.5.6) - Bootlin" + # deeper paths: "Makefile - arch/um/Makefile - Linux source code (v5.5.6) - Bootlin" + if path == '': + title_path = '' + elif len(path_split) == 1: + title_path = f'{ path_split[0] } - ' + else: + title_path = f'{ path_split[-1] } - { "/".join(path_split) } - ' + + get_url_with_new_version = lambda v: stringify_source_path(project, v, path) + get_diff_url = lambda v_other: stringify_diff_path(project, version, v_other, path) + + # Create template context + data = { + **get_layout_template_context(q, ctx, get_url_with_new_version, get_diff_url, project, version), + **template_ctx, + + 'other_version_path': find_version_path(get_versions_cached(q, ctx, project), version_other), + 'diff_mode_available': True, + 'diff_checked': True, + 'diff_exit_url': stringify_source_path(project, version, path), + + 'title_path': title_path, + 'path': path, + 'breadcrumb_links': breadcrumb_links, + 'base_url': diff_base_url, + } + + return (status, template.render(data)) + # Represents line in a file with URL to that line LineWithURL = namedtuple('LineWithURL', 'lineno, url') @@ -733,7 +1057,7 @@ def generate_ident_page(ctx: RequestContext, q: Query, get_url_with_new_version = lambda v: stringify_ident_path(project, v, family, ident) data = { - **get_layout_template_context(q, ctx, get_url_with_new_version, project, version), + **get_layout_template_context(q, ctx, get_url_with_new_version, None, project, version), 'searched_ident': ident, 'current_family': family, @@ -812,6 +1136,8 @@ def get_application(): app.add_route('/{project}/{version}/ident', IdentPostRedirectResource()) app.add_route('/{project}/{version}/ident/{ident}', IdentWithoutFamilyResource()) app.add_route('/{project}/{version}/{family}/ident/{ident}', IdentResource()) + app.add_route('/{project}/{version}/diff/{version_other}/{path:path}', DiffResource()) + app.add_route('/{project}/{version}/diff/{version_other}', DiffResource()) app.add_route('/acp', AutocompleteResource()) app.add_route('/api/ident/{project:project}/{ident:ident}', ApiIdentGetterResource()) diff --git a/elixir/web_utils.py b/elixir/web_utils.py index d2d038b7..bf57e5fc 100644 --- a/elixir/web_utils.py +++ b/elixir/web_utils.py @@ -8,6 +8,7 @@ import jinja2 from .lib import validFamily, run_cmd +from pygments.formatters import HtmlFormatter ELIXIR_DIR = os.path.normpath(os.path.dirname(__file__) + "/../") ELIXIR_REPO_LINK = 'https://github.com/bootlin/elixir/' @@ -81,3 +82,94 @@ def convert(self, value: str) -> str|None: value = parse.unquote(value) return validate_ident(value) +class DiffFormater(HtmlFormatter): + def __init__(self, diff, left: bool, *args, **kwargs): + self.diff = diff + self.left = left + super().__init__(*args[2:], **kwargs) + + def get_next_diff_line(self, diff_num, next_diff_line): + next_diff = self.diff[diff_num] if len(self.diff) > diff_num else None + + if next_diff is not None: + if self.left and (next_diff[0] == '-' or next_diff[0] == '+'): + next_diff_line = next_diff[1] + elif next_diff[0] == '-' or next_diff[0] == '+': + next_diff_line = next_diff[2] + elif self.left and next_diff[0] == '=': + next_diff_line = next_diff[1] + elif next_diff[0] == '=': + next_diff_line = next_diff[3] + else: + raise Exception("invlaid next diff mode") + + return next_diff, diff_num+1, next_diff_line + + def mark_line(self, line, css_class): + yield line[0], f'<span class="{css_class}">{line[1]}</span>' + + def mark_lines(self, source, num, css_class): + i = 0 + while i < num: + try: + t, line = next(source) + except StopIteration: + break + if t == 1: + yield t, f'<span class="{css_class}">{line}</span>' + i += 1 + else: + yield t, line + + def yield_empty(self, num): + for _ in range(num): + yield 0, '<span class="diff-line"> \n</span>' + + def wrap_diff(self, source): + next_diff, diff_num, next_diff_line = self.get_next_diff_line(0, None) + + linenum = 0 + + while True: + try: + line = next(source) + except StopIteration: + break + + if linenum == next_diff_line: + if next_diff is not None: + if self.left and next_diff[0] == '+': + yield from self.yield_empty(next_diff[3]) + yield line + linenum += 1 + elif next_diff[0] == '+': + yield from self.mark_line(line, 'line-added') + yield from self.mark_lines(source, next_diff[3]-1, 'line-added') + linenum += next_diff[3] + elif self.left and next_diff[0] == '-': + yield from self.mark_line(line, 'line-removed') + yield from self.mark_lines(source, next_diff[3]-1, 'line-removed') + linenum += next_diff[3] + elif next_diff[0] == '-': + yield from self.yield_empty(next_diff[3]) + yield line + linenum += 1 + elif next_diff[0] == '=': + total = max(next_diff[2], next_diff[4]) + to_print = next_diff[2] if self.left else next_diff[4] + yield from self.mark_line(line, 'line-removed' if self.left else 'line-added') + yield from self.mark_lines(source, to_print-1, 'line-removed' if self.left else 'line-added') + yield from self.yield_empty(total-to_print) + linenum += to_print + else: + yield line + linenum += 1 + + next_diff, diff_num, next_diff_line = self.get_next_diff_line(diff_num, next_diff_line) + else: + yield line + linenum += 1 + + def wrap(self, source): + return super().wrap(self.wrap_diff(source)) + diff --git a/projects/zephyr.sh b/projects/zephyr.sh index 149b385d..9ada9c94 100644 --- a/projects/zephyr.sh +++ b/projects/zephyr.sh @@ -1,7 +1,7 @@ # Elixir definitions for Zephyr # Enable DT bindings compatible strings support -dts_comp_support=1 +dts_comp_support=0 list_tags() { diff --git a/script.sh b/script.sh index 3bbff2a7..4b209ce2 100755 --- a/script.sh +++ b/script.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # This file is part of Elixir, a source code cross-referencer. # @@ -79,6 +79,12 @@ get_blob() git cat-file blob $opt1 } +get_blob_id() +{ + v=`echo $opt1 | version_rev` + git ls-tree --format='%(objectname)' "$v" "`denormalize $opt2`" 2>/dev/null +} + get_file() { v=`echo $opt1 | version_rev` @@ -89,11 +95,24 @@ get_dir() { v=`echo $opt1 | version_rev` git ls-tree -l "$v:`denormalize $opt2`" 2>/dev/null | - awk '{print $2" "$5" "$4" "$1}' | + awk '{print $2" "$5" "$4" "$1" "$3}' | grep -v ' \.' | sort -t ' ' -k 1,1r -k 2,2 } +get_diff() +{ + v=`echo $opt1 | version_rev` + v_other=`echo $opt2 | version_rev` + diff \ + --unchanged-group-format= \ + --new-group-format="+%de:%dE:%dN%c'\012'" \ + --old-group-format="-%de:%dE:%dn%c'\012'" \ + --changed-group-format="=%de:%dn:%dE:%dN%c'\012'" \ + <(git cat-file blob "$v:`denormalize $opt3`" 2>/dev/null) \ + <(git cat-file blob "$v_other:`denormalize $opt3`" 2>/dev/null) +} + tokenize_file() { if [ "$opt1" = -b ]; then @@ -259,6 +278,10 @@ case $cmd in get_blob ;; + get-blob-id) + get_blob_id + ;; + get-file) get_file ;; @@ -267,6 +290,10 @@ case $cmd in get_dir ;; + get-diff) + get_diff + ;; + list-blobs) list_blobs ;; diff --git a/static/script.js b/static/script.js index 62a54fc0..129b219c 100644 --- a/static/script.js +++ b/static/script.js @@ -113,13 +113,19 @@ function setupSidebarSwitch() { }); } -// Parse and validate line identifier in format L${number} -function parseLineId(lineId) { - if (lineId[0] != "L") { - return; +function getPrefix(lineId) { + if (lineId[0] == "L") { + return "L"; + } else if (lineId.startsWith("OL")) { + return "OL"; + } else { + return ""; } +} - let lineIdNum = parseInt(lineId.substring(1)); +// Parse and validate line identifier in format L${number} +function parseLineId(lineId) { + const lineIdNum = parseInt(lineId.substring(getPrefix(lineId).length)); console.assert(!isNaN(lineIdNum), "Invalid line id"); let lineElement = document.getElementById(lineId); @@ -151,18 +157,24 @@ function parseLineRangeAnchor(hashStr) { firstLine = lineTmp; } - return [firstLine, lastLine]; + return [firstLine, lastLine, getPrefix(hash[0])]; } // Highlights line number elements from firstLine to lastLine -function highlightFromTo(firstLine, lastLine) { - const firstLineElement = document.getElementById(`L${ firstLine }`); - const lastLineElement = document.getElementById(`L${ lastLine }`); - - const firstCodeLine = document.getElementById(`codeline-${ firstLine }`); - const lastCodeLine = document.getElementById(`codeline-${ lastLine }`); +function highlightFromTo(firstLine, lastLine, prefix='L') { + const firstLineElement = document.getElementById(`${ prefix }${ firstLine }`); + const lastLineElement = document.getElementById(`${ prefix }${ lastLine }`); + + let firstCodeLine = firstLineElement.parentNode; + // handle line-added/changed/deleted wrappers + if (firstCodeLine.parentNode.tagName != "PRE") { + firstCodeLine = firstCodeLine.parentNode; + } + let lastCodeLine = lastLineElement.parentNode; + if (lastCodeLine.parentNode.tagName != "PRE") { + lastCodeLine = lastCodeLine.parentNode; + } - addClassToRangeOfElements(firstLineElement.parentNode, lastLineElement.parentNode, "line-highlight"); addClassToRangeOfElements(firstCodeLine, lastCodeLine, "line-highlight"); } @@ -185,14 +197,14 @@ function addClassToRangeOfElements(first, last, class_name) { // Sets up listeners on element that contains line numbers to handle // shift-clicks for range highlighting function setupLineRangeHandlers() { - // Check if page contains the element with line numbers + // Check if page contains the element with line numbers and code // If not, then likely script is not executed in context of the source page - const linenodiv = document.querySelector(".linenodiv"); - if (linenodiv === null) { + const lxrcode = document.querySelectorAll(".lxrcode"); + if (lxrcode === null) { return; } - let rangeStartLine, rangeEndLine; + let rangeStartLine, rangeEndLine, rangePrefix; const parseFromHash = () => { const highlightedRange = parseLineRangeAnchor(window.location.hash); @@ -200,10 +212,15 @@ function setupLineRangeHandlers() { if (highlightedRange !== undefined) { rangeStartLine = highlightedRange[0]; rangeEndLine = highlightedRange[1]; - highlightFromTo(rangeStartLine, rangeEndLine); - document.getElementById(`L${rangeStartLine}`).scrollIntoView(); + rangePrefix = highlightedRange[2]; + highlightFromTo(rangeStartLine, rangeEndLine, rangePrefix); + const wrapper = document.querySelector('.wrapper'); + const offsetTop = document.getElementById(`${rangePrefix}${rangeStartLine}`).offsetTop; + wrapper.scrollTop = offsetTop < 100 ? 200 : offsetTop + 100; } else if (location.hash !== "" && location.hash[1] === "L") { rangeStartLine = parseLineId(location.hash.substring(1)); + } else if (location.hash !== "" && location.hash.startsWith("OL")) { + rangeStartLine = parseLineId(location.hash.substring(2)); } } @@ -214,25 +231,32 @@ function setupLineRangeHandlers() { parseFromHash(); - linenodiv.addEventListener("click", ev => { + lxrcode.forEach(el => el.addEventListener("click", ev => { if (ev.ctrlKey || ev.metaKey) { return; } + let el = ev.target; + if (el.classList.contains('linenos')) { + el = el.parentNode; + } + if (!el.classList.contains('line-link')) { + return; + } ev.preventDefault(); // Handler is set on the element that contains all line numbers, check if the // event is directed at an actual line number element - const el = ev.target; - if (typeof(el.id) !== "string" || el.id[0] !== "L" || el.tagName !== "A") { + if (typeof(el.id) !== "string" || !(el.id[0] == "L" || el.id.startsWith("OL")) || el.tagName !== "A") { return; } clearRangeHighlight(); - if (rangeStartLine === undefined || !ev.shiftKey) { + if (rangeStartLine === undefined || !ev.shiftKey || rangePrefix != getPrefix(el.id)) { rangeStartLine = parseLineId(el.id); + rangePrefix = getPrefix(el.id); rangeEndLine = undefined; - highlightFromTo(rangeStartLine, rangeStartLine); + highlightFromTo(rangeStartLine, rangeStartLine, rangePrefix); window.location.hash = el.id; } else if (ev.shiftKey) { if (rangeEndLine === undefined) { @@ -268,10 +292,10 @@ function setupLineRangeHandlers() { } } - highlightFromTo(rangeStartLine, rangeEndLine); - window.location.hash = `L${rangeStartLine}-L${rangeEndLine}`; + highlightFromTo(rangeStartLine, rangeEndLine, rangePrefix); + window.location.hash = `${rangePrefix}${rangeStartLine}-${rangePrefix}${rangeEndLine}`; } - }); + })); } /* Other fixes */ diff --git a/static/style.css b/static/style.css index 77d7703c..6dad8781 100644 --- a/static/style.css +++ b/static/style.css @@ -62,7 +62,6 @@ pre { font-size: 0.9em; line-height: 1.2; padding: 0; - color: #787878; } table { @@ -454,14 +453,49 @@ h2 { margin: 0; } .sidebar nav li.active a { - color: #eee; color: #6d7dd2; font-weight: 700; } +.sidebar nav li.active-other a { + color: #d2c26d; + font-weight: 700; +} .sidebar nav ul { padding: 0; } +#diff-checkbox-label { + color: #888; +} + +#diff-exit { + color: #888; + padding-left: 1em; +} + +#diff-exit:hover { + color: #eee; + color: #6d7dd2; + font-weight: 700; +} + +#diff-checkbox { + padding: 1em; + margin-bottom: 0.5em; +} + +#diff-checkbox:checked ~ li .version-link-source { + display: none; +} + +.version-link-diff { + display: none; +} + +#diff-checkbox:checked ~ li .version-link-diff { + display: inline; +} + /* reference popup */ #reference-popup-wrapper { @@ -695,6 +729,10 @@ h2 { color: gray; } +.workspace { + position: relative; +} + /* ident */ .lxrident { @@ -762,9 +800,18 @@ h2 { display: block; padding: 0.15em 1.5em; } +.lxrtree tr { + opacity: 1; +} .lxrtree tr:hover { background: #ddd; } + +.lxrtree .warning { + opacity: 0.5; + margin-left: 1.5em; +} + .tree-icon:before { display: inline-block; width: 1.5em; @@ -783,18 +830,40 @@ h2 { .tree-icon.icon-back:hover { opacity: 1; } + .size { - opacity: 0.6; + color: rgba(0, 0, 0, 0.6); min-height: 1.5em; /* line_height + 2 * padding = 1.2em + 2 * 0.15em */ text-align: right; } +.file-added { + background: #c2ffad; +} -/* source code */ +.lxrtree .file-added:hover { + background: #c9e9c0; +} -.lxrcode { - position: relative; +.file-removed { + background: #fcc0c0; } + +.lxrtree .file-removed:hover { + background: #e7c8c8; +} + +.file-changed { + background: #f8edc3; +} + +.lxrtree .file-changed:hover { + background: #e5dfca; +} + + +/* source code */ + .lxrcode pre { margin: 0; padding-top: 1em; @@ -806,6 +875,14 @@ h2 { vertical-align: top; } +.lxrcode .note-banner { + width: 100%; + text-align: center; + padding: 0.3em; + background-color: #e9e9e9; + color: #555; +} + .lxrcode, .highlighttable, .linenodiv, @@ -813,12 +890,74 @@ h2 { height: 100%; } +#L1 { + padding-top: 1em; +} + +.diff { + display: grid; + grid-template-columns: 50% 50%; +} + +.diff-line { + display: block; +} + +.diff .highlight pre span:first-child { +} + +.diff .lxrcode pre { + padding-top: 0; +} + +.line-added { + width: 100%; + height: 100%; + background: #C2FFAD; + display: block; +} + +.line-highlight.line-added .line-link { + background: #C2FFAD; +} + +.line-removed { + width: 100%; + height: 100%; + background: #fcc0c0; + display: block; +} + +.line-highlight.line-removed .line-link { + background: #fcc0c0; +} + span[id^='codeline-'] { display: block; padding-left: 1em; + width: 100%; + height: 100%; } -span[id^='codeline-'].line-highlight { +span[id^='codeline-'] { + padding-left: unset; +} + +span[id^='codeline-'] span { + display: inline-block; +} + +.line-link { + background: #e9e9e9; + color: #999; + padding-left: 1em; + padding-right: 1em; + margin-right: 1em; + width: 100%; + height: 100%; +} + +span.line-highlight { width: 100%; height: 100%; background: #f8edc3; @@ -836,9 +975,6 @@ span[id^='codeline-'].line-highlight { scroll-margin: 15vh; scroll-margin-top: 15vh; } -.line-highlight { - background: #ccc; -} .linenodiv pre a.line-highlight, .linenodiv pre span { display: inline-block; @@ -874,6 +1010,29 @@ span[id^='codeline-'].line-highlight { border-bottom: 1px dotted #000; } +.highlight .source-link, +.highlight .ident { + color: inherit; + font-weight: 700; + background: linear-gradient(to bottom, #0000 10%, #f4f6ff 10%, #f4f6ff 90%, #0000 90%); + border-radius: 0.2em; +} +.highlight .line-removed a { + background: #fcc0c0; +} +.highlight .line-added a { + background: #c2ffad; +} +.highlight a:hover { + border-bottom: 1px dotted #000; +} + +.highlight a .linenos { + font-weight: normal; + border-radius: 0; + background: none; +} + .code > div { height: 100%; } @@ -950,7 +1109,7 @@ span[id^='codeline-'].line-highlight { /* highlight */ -.highlight .code pre { +.highlight pre { color: #000; -moz-tab-size: 8; -o-tab-size: 8; @@ -966,76 +1125,76 @@ span[id^='codeline-'].line-highlight { hyphens: none; } -.highlight .code .hll { background-color: #ffffcc } -.highlight .code { background: #ffffff } -.highlight .code .c { color: slategray; font-style: italic; } /* Comment */ -.highlight .code .err { color: #FF0000; background-color: #FFAAAA } /* Error */ -.highlight .code .k { color: #008800 } /* Keyword */ -.highlight .code .o { color: #666 } /* Operator */ -.highlight .code .ch { color: #888888 } /* Comment.Hashbang */ -.highlight .code .cm { color: slategray; font-style: italic; } /* Comment.Multiline */ -.highlight .code .cp { color: #557799 } /* Comment.Preproc */ -.highlight .code .cpf { color: #888888 } /* Comment.PreprocFile */ -.highlight .code .c1 { color: slategray; font-style: italic; } /* Comment.Single */ -.highlight .code .cs { color: #cc0000 } /* Comment.Special */ -.highlight .code .gd { color: #A00000 } /* Generic.Deleted */ -.highlight .code .ge { font-style: italic } /* Generic.Emph */ -.highlight .code .gr { color: #FF0000 } /* Generic.Error */ -.highlight .code .gh { color: #000080 } /* Generic.Heading */ -.highlight .code .gi { color: #00A000 } /* Generic.Inserted */ -.highlight .code .go { color: #888888 } /* Generic.Output */ -.highlight .code .gp { color: #c65d09 } /* Generic.Prompt */ -.highlight .code .gs { font-weight: bold } /* Generic.Strong */ -.highlight .code .gu { color: #800080 } /* Generic.Subheading */ -.highlight .code .gt { color: #0044DD } /* Generic.Traceback */ -.highlight .code .kc { color: #008800 } /* Keyword.Constant */ -.highlight .code .kd { color: #008800 } /* Keyword.Declaration */ -.highlight .code .kn { color: #008800 } /* Keyword.Namespace */ -.highlight .code .kp { color: #003388 } /* Keyword.Pseudo */ -.highlight .code .kr { color: #008800 } /* Keyword.Reserved */ -.highlight .code .kt { color: #333399 } /* Keyword.Type */ -.highlight .code .m { color: #6600EE } /* Literal.Number */ -.highlight .code .s { color: #de7f00 } /* Literal.String */ -.highlight .code .na { color: #0000CC } /* Name.Attribute */ -.highlight .code .nb { color: #007020 } /* Name.Builtin */ -.highlight .code .nc { color: #BB0066 } /* Name.Class */ -.highlight .code .no { color: #003366 } /* Name.Constant */ -.highlight .code .nd { color: #555555 } /* Name.Decorator */ -.highlight .code .ni { color: #880000 } /* Name.Entity */ -.highlight .code .ne { color: #FF0000 } /* Name.Exception */ -.highlight .code .nf { color: #0066BB } /* Name.Function */ -.highlight .code .nl { color: #997700 } /* Name.Label */ -.highlight .code .nn { color: #0e84b5 } /* Name.Namespace */ -.highlight .code .nt { color: #007700 } /* Name.Tag */ -.highlight .code .nv { color: #996633 } /* Name.Variable */ -.highlight .code .ow { color: #000000 } /* Operator.Word */ -.highlight .code .p { color: #666 } /* Text.Punctuation */ -.highlight .code .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .code .mb { color: #6600EE } /* Literal.Number.Bin */ -.highlight .code .mf { color: #6600EE } /* Literal.Number.Float */ -.highlight .code .mh { color: #005588 } /* Literal.Number.Hex */ -.highlight .code .mi { color: #0000DD } /* Literal.Number.Integer */ -.highlight .code .mo { color: #4400EE } /* Literal.Number.Oct */ -.highlight .code .sa { color: #de7f00 } /* Literal.String.Affix */ -.highlight .code .sb { color: #de7f00 } /* Literal.String.Backtick */ -.highlight .code .sc { color: #de7f00 } /* Literal.String.Char */ -.highlight .code .dl { color: #de7f00 } /* Literal.String.Delimiter */ -.highlight .code .sd { color: #a29900 } /* Literal.String.Doc */ -.highlight .code .s2 { color: #de7f00 } /* Literal.String.Double */ -.highlight .code .se { color: #a29900 } /* Literal.String.Escape */ -.highlight .code .sh { color: #de7f00 } /* Literal.String.Heredoc */ -.highlight .code .si { color: #de7f00 } /* Literal.String.Interpol */ -.highlight .code .sx { color: #de7f00 } /* Literal.String.Other */ -.highlight .code .sr { color: #a29900 } /* Literal.String.Regex */ -.highlight .code .s1 { color: #de7f00 } /* Literal.String.Single */ -.highlight .code .ss { color: #a29900 } /* Literal.String.Symbol */ -.highlight .code .bp { color: #007020 } /* Name.Builtin.Pseudo */ -.highlight .code .fm { color: #0066BB } /* Name.Function.Magic */ -.highlight .code .vc { color: #336699 } /* Name.Variable.Class */ -.highlight .code .vg { color: #dd7700 } /* Name.Variable.Global */ -.highlight .code .vi { color: #3333BB } /* Name.Variable.Instance */ -.highlight .code .vm { color: #996633 } /* Name.Variable.Magic */ -.highlight .code .il { color: #0000DD } /* Literal.Number.Integer.Long */ +.highlight .hll { background-color: #ffffcc } +.highlight { background: #ffffff } +.highlight .c { color: slategray; font-style: italic; } /* Comment */ +.highlight .err { color: #FF0000; background-color: #FFAAAA } /* Error */ +.highlight .k { color: #008800 } /* Keyword */ +.highlight .o { color: #666 } /* Operator */ +.highlight .ch { color: #888888 } /* Comment.Hashbang */ +.highlight .cm { color: slategray; font-style: italic; } /* Comment.Multiline */ +.highlight .cp { color: #557799 } /* Comment.Preproc */ +.highlight .cpf { color: #888888 } /* Comment.PreprocFile */ +.highlight .c1 { color: slategray; font-style: italic; } /* Comment.Single */ +.highlight .cs { color: #cc0000 } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #FF0000 } /* Generic.Error */ +.highlight .gh { color: #000080 } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #888888 } /* Generic.Output */ +.highlight .gp { color: #c65d09 } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080 } /* Generic.Subheading */ +.highlight .gt { color: #0044DD } /* Generic.Traceback */ +.highlight .kc { color: #008800 } /* Keyword.Constant */ +.highlight .kd { color: #008800 } /* Keyword.Declaration */ +.highlight .kn { color: #008800 } /* Keyword.Namespace */ +.highlight .kp { color: #003388 } /* Keyword.Pseudo */ +.highlight .kr { color: #008800 } /* Keyword.Reserved */ +.highlight .kt { color: #333399 } /* Keyword.Type */ +.highlight .m { color: #6600EE } /* Literal.Number */ +.highlight .s { color: #de7f00 } /* Literal.String */ +.highlight .na { color: #0000CC } /* Name.Attribute */ +.highlight .nb { color: #007020 } /* Name.Builtin */ +.highlight .nc { color: #BB0066 } /* Name.Class */ +.highlight .no { color: #003366 } /* Name.Constant */ +.highlight .nd { color: #555555 } /* Name.Decorator */ +.highlight .ni { color: #880000 } /* Name.Entity */ +.highlight .ne { color: #FF0000 } /* Name.Exception */ +.highlight .nf { color: #0066BB } /* Name.Function */ +.highlight .nl { color: #997700 } /* Name.Label */ +.highlight .nn { color: #0e84b5 } /* Name.Namespace */ +.highlight .nt { color: #007700 } /* Name.Tag */ +.highlight .nv { color: #996633 } /* Name.Variable */ +.highlight .ow { color: #000000 } /* Operator.Word */ +.highlight .p { color: #666 } /* Text.Punctuation */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mb { color: #6600EE } /* Literal.Number.Bin */ +.highlight .mf { color: #6600EE } /* Literal.Number.Float */ +.highlight .mh { color: #005588 } /* Literal.Number.Hex */ +.highlight .mi { color: #0000DD } /* Literal.Number.Integer */ +.highlight .mo { color: #4400EE } /* Literal.Number.Oct */ +.highlight .sa { color: #de7f00 } /* Literal.String.Affix */ +.highlight .sb { color: #de7f00 } /* Literal.String.Backtick */ +.highlight .sc { color: #de7f00 } /* Literal.String.Char */ +.highlight .dl { color: #de7f00 } /* Literal.String.Delimiter */ +.highlight .sd { color: #a29900 } /* Literal.String.Doc */ +.highlight .s2 { color: #de7f00 } /* Literal.String.Double */ +.highlight .se { color: #a29900 } /* Literal.String.Escape */ +.highlight .sh { color: #de7f00 } /* Literal.String.Heredoc */ +.highlight .si { color: #de7f00 } /* Literal.String.Interpol */ +.highlight .sx { color: #de7f00 } /* Literal.String.Other */ +.highlight .sr { color: #a29900 } /* Literal.String.Regex */ +.highlight .s1 { color: #de7f00 } /* Literal.String.Single */ +.highlight .ss { color: #a29900 } /* Literal.String.Symbol */ +.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #0066BB } /* Name.Function.Magic */ +.highlight .vc { color: #336699 } /* Name.Variable.Class */ +.highlight .vg { color: #dd7700 } /* Name.Variable.Global */ +.highlight .vi { color: #3333BB } /* Name.Variable.Instance */ +.highlight .vm { color: #996633 } /* Name.Variable.Magic */ +.highlight .il { color: #0000DD } /* Literal.Number.Integer.Long */ /* layout */ @@ -1235,7 +1394,7 @@ main { position: sticky; left: 0; } - .js .linenos { + .js .line-link { position: -webkit-sticky; position: sticky; left: 0; @@ -1529,7 +1688,7 @@ main { } .nav-links a:hover { color: #fff; -} +)} .social-icons { position: absolute; diff --git a/templates/diff.html b/templates/diff.html new file mode 100644 index 00000000..bc619224 --- /dev/null +++ b/templates/diff.html @@ -0,0 +1,23 @@ +{% extends "layout.html" %} + +{% block title %} + {{ title_path }} {{ current_project|capitalize }} diff {{ current_tag }} - Bootlin Elixir Cross Referencer +{% endblock %} + +{% block description -%} + Elixir Cross Referencer - diff of {{ current_project|capitalize }} {{ current_tag -}} {{- '' if path|length <= 1 else ': ' + path[1:] }} +{%- endblock %} + +{% block main %} +<div class="diff"> + <div class="lxrcode"> + <div class="note-banner">{{ current_tag }}</div> + {{ code }} + </div> + <div class="lxrcode"> + <div class="note-banner">{{ other_tag }}</div> + {{ code_other }} + </div> +</div> +{% endblock %} + diff --git a/templates/sidebar.html b/templates/sidebar.html index dc9eebce..1c4b78ac 100644 --- a/templates/sidebar.html +++ b/templates/sidebar.html @@ -19,10 +19,20 @@ <h3 class="screenreader">Projects</h3> <h3 class="screenreader">Versions</h3> <ul class="versions"> + {% if diff_mode_available %} + <input id="diff-checkbox" type="checkbox" {% if diff_checked %} checked {% endif %} /> + <label id="diff-checkbox-label" for="diff-checkbox">Diff mode</label> + {% if diff_exit_url %} + <a id="diff-exit" href="{{ diff_exit_url }}">Exit diff mode</a> + {% endif %} + {% endif %} + {% set current_major, current_minor, current_version = current_version_path %} + {% set other_major, other_minor, other_version = other_version_path %} {% for major, major_versions in (versions|default({})).items() %} <li> - <span class="{{ 'active' if current_major == major else '' }}">{{ major }}</span> + <span class="{{ 'active' if major in (current_major, other_major) else '' }}">{{ major }}</span> + <ul> {% for minor, minor_versions in major_versions.items() %} {% if minor == minor_versions[0] and minor_versions|length == 1 %} @@ -31,13 +41,21 @@ <h3 class="screenreader">Versions</h3> </li> {% else %} <li> - <span class="{{ 'active' if minor == current_minor else '' }}">{{ minor }}</span> + <span class="{{ 'active' if minor in (current_minor, other_minor) else '' }}">{{ minor }}</span> <ul> {% for v in minor_versions %} - <li class="li-link {{ 'active' if v.version == current_tag else '' }}"> - <a href="{{ v.url }}"> + <li class="li-link + {{ 'active' if v.version == current_tag else '' }} + {{ 'active-other' if v.version == other_tag else '' }} + "> + <a class="version-link-source" href="{{ v.url }}"> {{ v.version }} </a> + {% if v.diff_url is not none %} + <a class="version-link-diff" href="{{ v.diff_url }}"> + {{ v.version }} + </a> + {% endif %} </li> {% endfor %} </ul> diff --git a/templates/source.html b/templates/source.html index 1ad88544..e06c1bd2 100644 --- a/templates/source.html +++ b/templates/source.html @@ -10,6 +10,9 @@ {% block main %} <div class="lxrcode"> + {% if warning %} + <div class="note-banner">{{ warning }}</div> + {% endif %} {{ code }} </div> {% endblock %} diff --git a/templates/topbar.html b/templates/topbar.html index 171b107e..df37ff5c 100644 --- a/templates/topbar.html +++ b/templates/topbar.html @@ -1,8 +1,8 @@ <header class="topbar"> <div class="breadcrumb"> <a href="#menu" class="open-menu icon-menu screenreader" title="Open Menu">Open Menu</a> - <a class="project" href="{{ source_base_url }}">/</a> - {% for name, url in breadcrumb_urls %} + <a class="project" href="{{ source_base_url if not base_url else base_url }}">/</a> + {% for name, url in breadcrumb_links %} <a href="{{ url }}">{{ name }}</a> {{ '/' if not loop.last else '' }} {% endfor %} diff --git a/templates/tree.html b/templates/tree.html index ef9755e4..4339ecda 100644 --- a/templates/tree.html +++ b/templates/tree.html @@ -15,6 +15,11 @@ {% block main %} <div class="lxrtree"> + {% for warning in warnings %} + {% if warning is not none %} + <div class="warning">Note: {{ warning }}</div> + {% endif %} + {% endfor %} <table> <tbody> {% if back_url is not none %} @@ -23,10 +28,10 @@ <td><a tabindex="-1" class="size" href="{{ back_url }}"></a></td> </tr> {% endif %} - {% for type, name, path, url, size in dir_entries %} - <tr> + {% for type, name, path, url, size, cls in dir_entries %} + <tr class="{{ "file-"+cls if cls is not none else '' -}}"> <td> - <a class="tree-icon icon-{{- type if type != 'symlink' else 'blob' -}}" + <a class="tree-icon icon-{{- type if type != 'symlink' else 'blob' }}" href="{{ url }}"> {% if type == 'symlink' %} {{ name }} -> {{ path }}