Django/Jinja template indenter

Related tags

Djangodjhtml
Overview

DjHTML

A pure-Python Django/Jinja template indenter without dependencies.

DjHTML is a fully automatic template indenter that works with mixed HTML/CSS/Javascript templates that contain Django or Jinja template tags. It works similar to other code-formatting tools such as Black and interoperates nicely with pre-commit.

DjHTML is an indenter and not a formatter: it will only add/remove whitespace at the beginning of lines. It will not insert newlines or other characters. The goal is to correctly indent already well-structured templates, not to fix broken ones.

For example, consider the following incorrectly indented template:

<!doctype html>
<html>
    <body>
        {% block content %}
        Hello, world!
        {% endblock %}
        <script>
            $(function() {
            console.log('Hi mom!');
            });
        </script>
    </body>
</html>

This is what it will look like after processing by DjHTML:

<!doctype html>
<html>
    <body>
        {% block content %}
            Hello, world!
        {% endblock %}
        <script>
            $(function() {
                console.log('Hi mom!');
            });
        </script>
    </body>
</html>

Installation

Install DjHTML with the following command:

$ pip install djhtml

Usage

After installation you can indent templates using the djhtml command. The default is to write the indented output to standard out. To modify the source file in-place, use the -i / --in-place option:

$ djhtml -i template.html
reindented template.html
1 template has been reindented.

Normally, the exit status of 0 means everything went well, regardless of whether any files were changed. If any errors were encountered, the exit status indicated the number of problematic files. However, when the option -c / --check is used, the exit status is the number of files that would have changed, but no changes are actually made.

All available options are:

  • -h / --help: show overview of available options
  • -i / --in-place: modify files in-place
  • -c / --check: don't modify files; the exit status is the number of files that would have changed
  • -q / --quiet: don't print any output
  • -t / --tabwidth: set tabwidth (default is 4)
  • -o / --output-file: write output to specified file

fmt:off and fmt:on

You can exclude specific lines from being processed with the {# fmt:off #} and {# fmt:on #} operators:

<div class="
    {# fmt:off #}
      ,-._|\
     /     .\
     \_,--._/
    {# fmt:on #}
"/>

Contents inside <pre> ... </pre>, <!-- ... --->, /* ... */, and {% comment %} ... {% endcomment %} tags are also ignored (depending on the current mode).

Modes

The indenter operates in one of three different modes:

  • DjHTML mode: the default mode. Invoked by using the djhtml command or the pre-commit hook.

  • DjCSS mode. Will be entered when a <style> tag is encountered in DjHTML mode. It can also be invoked directly with the command djcss.

  • DjJS mode. Will be entered when a <script> tag is encountered in DjHTML mode. It can also be invoked directly with the command djjs.

pre-commit configuration

You can use DjHTML as a pre-commit hook to automatically indent your templates upon each commit.

First, install pre-commit:

$ pip install pre-commit
$ pre-commit install

Then, add the following to your .pre-commit-config.yaml:

repos:
- repo: https://github.com/rtts/djhtml
  rev: main
  hooks:
    - id: djhtml

Finally, run the following command:

$ pre-commit autoupdate

Now when you run git commit you will see something like the following output:

$ git commit

djhtml...................................................................Failed
- hook id: djhtml
- files were modified by this hook

reindented template.html
1 template has been reindented.

To inspect the changes that were made, use git diff. If you are happy with the changes, you can commit them normally. If you are not happy, please do the following:

  1. Run SKIP=djhtml git commit to commit anyway, skipping the djhtml hook.

  2. Consider opening an issue with the relevant part of the input file that was incorrectly formatted, and an example of how it should have been formatted.

Your feedback for improving DjHTML is very welcome!

Development

Use your preferred system for setting up a virtualenv, docker environment, or whatever else, then run the following:

python -m pip install -e .[dev]
pre-commit install --install-hooks

Tests can then be run quickly in that environment:

python -m unittest discover -v

Or testing in all available supported environments and linting can be run with nox:

nox
Comments
  • (S)CSS files get no indention

    (S)CSS files get no indention

    Hi there, just checked out this packge after the DjangoCon recommendation. It looks very nice but I guess I found an issue.

    The djcss is removing any indention of my SCSS files which makes it more or less unreadable.

    grafik

    Any ideas how I can fix this? Is there a setting to adjust the indention?

    Thx!

    cannot-reproduce 
    opened by GitRon 21
  • Too many open files when trying to edit multiple files

    Too many open files when trying to edit multiple files

    Hi,

    Thanks for creating this tool. I wanted to update all the templates of my project like this:

    find . -name "*.html" -type f -exec djhtml -i -t2 {} +, But it gives me this error:

    But I get this error:

    usage: djhtml [-h] [-i] [-q] [-t N] [-o filename] [filenames ...]
    djhtml: error: argument filenames: can't open './demo_cms/template/content_news.html': [Errno 24] Too many open files: './demo_cms/template/content_news.html'
    

    Please let me know if I am doing something wrong. I don't want to edit each file by hand.

    opened by vinitkumar 7
  • Config file to ignore some block tags

    Config file to ignore some block tags

    Hello. Firstly thanks for the library, we're trying it out on our projects and really like it so far.

    We have one small problem though. We use a markdown block filter, where the content of the block is parsed as markdown. This means initial space (as djhtml adds) is parsed as a blockquote.

    I know we can use the fmt control comments, but ideally we'd be able to automatically ignore anything inside {% filter markdown %}.

    I was going to raise a PR for this, but I realise that this is a very specific case. Rather than adding exemptions ad-hoc, it might be better to allow configuring blocks to be excluded by the user / project.

    Alternatively, it might be a good idea to slip formatting inside filter blocks altogether - but I don't know enough about how they're used in other projects to know if this would be a good idea.

    I'd be interested to hear your thoughts on this, and would be happy to submit a PR once we've worked out a good solution.

    opened by symroe 6
  • Could you also support jinja templates?

    Could you also support jinja templates?

    Thanks for building this, its been on my wish list for a while.

    I think it wouldn't be too hard to support Jinja2 templates as well (I think the syntax is very similar), would this be something you are willing to do?

    yes 
    opened by pgjones 6
  • Still pretty verbose when running `pre-commit run --all-files`

    Still pretty verbose when running `pre-commit run --all-files`

    When running pre-commit run --all-files it's still pretty verbose:

    djhtml...................................................................Failed
    - hook id: djhtml
    - files were modified by this hook
    
    All done! \o/
    16 templates left unchanged.
    All done! \o/
    16 templates left unchanged.
    All done! \o/
    16 templates left unchanged.
    All done! \o/
    16 templates left unchanged.
    All done! \o/
    16 templates left unchanged.
    All done! \o/
    16 templates left unchanged.
    reindented babeldat/core/templates/partials/_menu.html
    All done! \o/
    1 template reindented, 15 templates left unchanged.
    All done! \o/
    11 templates left unchanged.
    

    FYI: I had 16 files staged, only 4 of them were templates

    opened by richarddewit 5
  • In/dedentation is sometimes incorrect when multiple tags are on the same line

    In/dedentation is sometimes incorrect when multiple tags are on the same line

    Really impressed with this package!

    I noticed that in some of my longer templates in which I have sloppily at times had variations on <div><div> or </div><div> or </div></div> in the same line, things can get a bit weird. It's still valid html, but it's not pretty, and seems to trip djhtml up just a bit.

    For instance, this minimal example:

    {% block css %}
    {% endblock css %}{% block content %}
    <div class="row">
    <div class="input-group date" data-provide="datepicker" data-date-format="yyyy/mm/dd">
    <div class="btn">
    </div></div>
    </div><div>
    <div class="custom-control custom-switch">
    <input type="checkbox" name="filter-changes" id="filter-changes" class="custom-control-input fs-nano" value="0" {% if request.session.manage_filter_changes %}{% if request.session.manage_filter_changes == True %}checked{% endif %}{% endif %}>
    </div>
    <form class="form-inline">
    <div class="custom-control custom-switch">
    <input type="checkbox" name="filter-status-new" id="filter-status-new" class="custom-control-input fs-nano" value="0" {% if request.session.manage_filter_status_new %}{% if request.session.manage_filter_status_new == True %}checked{% endif %}{% endif %}>
    </div>
    </form>
    </div>
    {% endblock content %}
    

    I would expect something like this where the final div and {% endblock %} are fully left-justified:

    {% block css %}
    {% endblock css %}{% block content %}
    <div class="row">
        <div class="input-group date" data-provide="datepicker" data-date-format="yyyy/mm/dd">
            <div class="btn">
        </div></div>
    </div><div>
        <div class="custom-control custom-switch">
            <input type="checkbox" name="filter-changes" id="filter-changes" class="custom-control-input fs-nano" value="0" {% if request.session.manage_filter_changes %}{% if request.session.manage_filter_changes == True %}checked{% endif %}{% endif %}>
        </div>
        <form class="form-inline">
            <div class="custom-control custom-switch">
                <input type="checkbox" name="filter-status-new" id="filter-status-new" class="custom-control-input fs-nano" value="0" {% if request.session.manage_filter_status_new %}{% if request.session.manage_filter_status_new == True %}checked{% endif %}{% endif %}>
            </div>
        </form>
    </div>
    {% endblock content %}
    

    Or maybe this in a perfect world (though I suspect this would take a LOT more work to achieve via djhtml)

    {% block css %}
    {% endblock css %}
    {% block content %}
    <div class="row">
        <div class="input-group date" data-provide="datepicker" data-date-format="yyyy/mm/dd">
            <div class="btn">
            </div>
        </div>
    </div>
    <div>
        <div class="custom-control custom-switch">
            <input type="checkbox" name="filter-changes" id="filter-changes" class="custom-control-input fs-nano" value="0" {% if request.session.manage_filter_changes %}{% if request.session.manage_filter_changes == True %}checked{% endif %}{% endif %}>
        </div>
        <form class="form-inline">
            <div class="custom-control custom-switch">
                <input type="checkbox" name="filter-status-new" id="filter-status-new" class="custom-control-input fs-nano" value="0" {% if request.session.manage_filter_status_new %}{% if request.session.manage_filter_status_new == True %}checked{% endif %}{% endif %}>
            </div>
        </form>
    </div>
    {% endblock content %}
    

    But the actual output is this:

    {% block css %}
    {% endblock css %}{% block content %}
    <div class="row">
        <div class="input-group date" data-provide="datepicker" data-date-format="yyyy/mm/dd">
            <div class="btn">
        </div></div>
    </div><div>
        <div class="custom-control custom-switch">
            <input type="checkbox" name="filter-changes" id="filter-changes" class="custom-control-input fs-nano" value="0" {% if request.session.manage_filter_changes %}{% if request.session.manage_filter_changes == True %}checked{% endif %}{% endif %}>
            </div>
            <form class="form-inline">
                <div class="custom-control custom-switch">
                    <input type="checkbox" name="filter-status-new" id="filter-status-new" class="custom-control-input fs-nano" value="0" {% if request.session.manage_filter_status_new %}{% if request.session.manage_filter_status_new == True %}checked{% endif %}{% endif %}>
                    </div>
                </form>
            </div>
        {% endblock content %}
    

    I wish I were good enough with parsing/formatting to submit a PR, but hopefully this is at least helpful input.

    bug 
    opened by jacklinke 5
  • Indention bug with Stimulus action

    Indention bug with Stimulus action

    If we have a simple test.html file

    <div>
      <div data-action="click->modal#closeBackground">
      </div>
    </div>
    

    If I run djhtml -i test.html

    I got

    <div>
        <div data-action="click->modal#closeBackground">
        </div>
        </div>
    

    As you can see, the last </div> indention is not correct.

    The click->modal#closeBackground caused this problem.

    opened by michael-yin 4
  • Inserting final newlines isn’t safe

    Inserting final newlines isn’t safe

    I’ve been trying out djhtml on a project and our visual regression testing showed there were differences before-after the reformatting. Upon further inspection this seems to be due to djhtml inserting final newlines in some occurences.

    Looking at the code, this seems to be done on purpose, but there are two issues:

    • Whitespace is often sensitive in HTML – so inserting whitespace where there wasn’t any will change how the page renders.
    • The "insertion of final newline" logic isn’t consistent – djhtml will only add the newline when it made other changes to the template. It won’t add the newline if the template’s formatting is already good.

    Here is a practical example where this happens. With an icon.html template:

    <svg>
    <use href="#my-icon"></use>
    </svg>
    

    And a button.html:

    <button>{% include "icon.html" }My button</button>
    

    Without a final newline in icon.html, this renders as:

    <button<svg>
    <use href="#my-icon"></use>
    </svg>My button</button>
    

    With a final newline (which djhtml inserts) in icon.html, this becomes:

    <button<svg>
        <use href="#my-icon"></use>
    </svg>
    My button</button>
    

    This will cause additional space between the icon and the text. I’ve made a small demo to help illustrate.


    As a resolution to this, my preference would be for djhtml to completely stop inserting final newlines. There are a lot of tools that do this already for people who want to, and it’s not something that can be applied to existing HTML templates with full certainty as the behavior depends on how the partials are used.

    opened by thibaudcolas 4
  • Jinja

    Jinja "whitespace control" is not understood

    Jinja allows one to control whitespace generated by tags. For instance:

    <div>
        {% for item in sequence %}
            {{ item }}
        {%- endfor %}
    </div>
    

    but DjHTML then misses the endfor.

    opened by sjoerdjob 4
  • Support passing directories as command-line arguments

    Support passing directories as command-line arguments

    This makes it easier to run djhtml over a bunch of files in a directory or directories.

    The default suffixes are used as a filter with no configuration allowed (as black seems to work).

    I'm aware of #13 recommending using find ... | xargs or the usage of pre-commit. However, I would like a single command that works on Linux, MacOS, and crucially Windows systems (find and xargs don't work). This would allow me to recommend to users how to use djhtml without having to add lots of additional text explaining how to do it on different systems i.e. djhtml src/templates is all that is needed.

    opened by pgjones 3
  • Indentation of Internet Explorer conditional comment

    Indentation of Internet Explorer conditional comment

    The following HTML:

    <div>
    <!--[if lt IE 9]>
        <p>Not supported</p>
    <![endif]-->
    </div>
    

    gets indented as:

    <div>
        <!--[if lt IE 9]>
        <p>Not supported</p>
    <![endif]-->
    </div>
    

    I would have expected djhtml to apply the same amount of indentation to every line within the div. I imagine it’s IE conditional comments that are causing this – if so they probably don’t warrant any kind of support as they’re well past their expiration date. But I thought I’d report it anyway just in case.

    wontfix 
    opened by thibaudcolas 3
  • Indentation of multiline method chains in script tags

    Indentation of multiline method chains in script tags

    The following JS within HTML:

    <script>
    window.fetch('/test.html')
        .then((html) => {
            document.body.innerHTML = html;
        });
    </script>
    

    gets indented as:

    <script>
        window.fetch('/test.html')
            .then((html) => {
            document.body.innerHTML = html;
        });
    </script>
    

    I would have expected everything within the script tag to get one extra level of indentation. I’m not sure what is causing this, but I at least tried with both a function and the arrow function as above, so I suspect it’s the method chaining that might be the problem.

    help wanted 
    opened by thibaudcolas 1
  • Multi-line tag attribute alignment

    Multi-line tag attribute alignment

    I had some multi-line tags like:

    <img src="/some/long/path"
         alt="Some text">
    

    DjHTML indented them like so:

    <img src="/some/long/path"
      alt="Some text">
    

    I think it would be more nautral to retain the “attribute alignment”, and keep alt at the same column as src.

    Do you think this would be possible?

    Otherwise, the attribue-aligned style I found that works with DjHTML is:

    <img
      src="/some/long/path"
      alt="Some text">
    
    help wanted 
    opened by adamchainz 3
Releases(v1.5.2)
  • v1.5.2(Aug 4, 2022)

    What's Changed

    • Support passing directories as command-line arguments by @pgjones in https://github.com/rtts/djhtml/pull/64

    New Contributors

    • @pgjones made their first contribution in https://github.com/rtts/djhtml/pull/64

    Full Changelog: https://github.com/rtts/djhtml/compare/v1.5.1...v1.5.2

    Source code(tar.gz)
    Source code(zip)
  • v1.5.1(Jun 23, 2022)

    What's Changed

    • Upgrade Black by @rupertbaker in https://github.com/rtts/djhtml/pull/63
    • Fix interpretation of closing arrows in HTML attrs by @rupertbaker in https://github.com/rtts/djhtml/pull/62

    New Contributors

    • @rupertbaker made their first contribution in https://github.com/rtts/djhtml/pull/63

    Full Changelog: https://github.com/rtts/djhtml/compare/v1.5.0...v1.5.1

    Source code(tar.gz)
    Source code(zip)
  • v1.5.0(Feb 14, 2022)

    What's Changed

    The behavior regarding final newlines has changed between DjHTML v1.4.14 and v1.5.0. It used to always append the final newline, but now this will only happen when the source file already contains a final newline.

    See https://github.com/rtts/djhtml/issues/56 for the discussion that led to this change.

    If you still need the old behavior, the best option is to use https://github.com/pre-commit/pre-commit-hooks#end-of-file-fixer by adding the following to your .pre-commit-config.yaml:

    - repo: https://github.com/pre-commit/pre-commit-hooks
      rev: main  # replace with the latest tag on GitHub
      hooks:
        - id: end-of-file-fixer
    

    New Contributors

    • @thibaudcolas pointed out the inconsistent behavior in appending final newlines

    Full Changelog: https://github.com/rtts/djhtml/compare/v1.4.14...v1.5.0

    Source code(tar.gz)
    Source code(zip)
  • v1.4.14(Feb 8, 2022)

    What's Changed

    • Improve pre-commit hook of DjCSS by @DmytroLitvinov in https://github.com/rtts/djhtml/pull/55

    New Contributors

    • @DmytroLitvinov made their first contribution in https://github.com/rtts/djhtml/pull/55

    Full Changelog: https://github.com/rtts/djhtml/compare/v1.4.13...v1.4.14

    Source code(tar.gz)
    Source code(zip)
  • v1.4.13(Feb 6, 2022)

    What's Changed

    • Indent CSS functions by @neruson in https://github.com/rtts/djhtml/pull/52
    • Further indent HTML attribute values by @JaapJoris in https://github.com/rtts/djhtml/pull/54

    New Contributors

    • @neruson made their first contribution in https://github.com/rtts/djhtml/pull/52

    Full Changelog: https://github.com/rtts/djhtml/compare/v1.4.12...v1.4.13

    Source code(tar.gz)
    Source code(zip)
  • v1.4.12(Feb 4, 2022)

  • v1.4.11(Dec 20, 2021)

    What's Changed

    • Update pre-commit hooks by @adamchainz in https://github.com/rtts/djhtml/pull/45
    • Remove linting from nox by @adamchainz in https://github.com/rtts/djhtml/pull/46
    • Add CI run of nox on GitHub Actions by @adamchainz in https://github.com/rtts/djhtml/pull/47
    • Recommend using tags for pre-commit by @adamchainz in https://github.com/rtts/djhtml/pull/44
    • Added support for JavaScript switch statement. by @pawelpel in https://github.com/rtts/djhtml/pull/49

    New Contributors

    • @pawelpel made their first contribution in https://github.com/rtts/djhtml/pull/49

    Full Changelog: https://github.com/rtts/djhtml/compare/v1.4.10...v1.4.11

    Source code(tar.gz)
    Source code(zip)
  • v1.4.10(Nov 9, 2021)

    What's Changed

    • Support Python 3.10 by @adamchainz in https://github.com/rtts/djhtml/pull/43

    New Contributors

    • @adamchainz made their first contribution in https://github.com/rtts/djhtml/pull/43

    Full Changelog: https://github.com/rtts/djhtml/compare/v1.4.9...v1.4.10

    Source code(tar.gz)
    Source code(zip)
  • v1.4.9(Jun 16, 2021)

  • v1.4.8(Jun 7, 2021)

  • v1.4.7(Jun 4, 2021)

  • v1.4.6(Jun 3, 2021)

  • v1.4.5(May 28, 2021)

  • v1.4.4(May 28, 2021)

  • v1.4.3(May 26, 2021)

  • v1.4.2(May 25, 2021)

  • v1.4.1(May 25, 2021)

  • v1.4.0(May 23, 2021)

  • v1.3.0(May 23, 2021)

  • v1.2.2(May 18, 2021)

  • v1.2.1(May 17, 2021)

  • v1.2.0(May 17, 2021)

  • v1.0.2(May 16, 2021)

Owner
Return to the Source
Smart online platforms for innovative businesses + fun side projects
Return to the Source
Bootstrap 3 integration with Django.

django-bootstrap3 Bootstrap 3 integration for Django. Goal The goal of this project is to seamlessly blend Django and Bootstrap 3. Want to use Bootstr

Zostera B.V. 2.3k Jan 03, 2023
A Student/ School management application built using Django and Python.

Student Management An awesome student management app built using Django.! Explore the docs » View Demo · Report Bug · Request Feature Table of Content

Nishant Sethi 1 Feb 10, 2022
Awesome Django Blog App

Awesome-Django-Blog-App Made with love django as the backend and Bootstrap as the frontend ! i hope that can help !! Project Title Django provides mul

ANAS NABIL 2 Feb 08, 2022
Django-fast-export - Utilities for quickly streaming CSV responses to the client

django-fast-export Utilities for quickly streaming CSV responses to the client T

Matthias Kestenholz 4 Aug 24, 2022
A CBV to handle multiple forms in one view

django-shapeshifter A common problem in Django is how to have a view, especially a class-based view that can display and process multiple forms at onc

Kenneth Love 167 Nov 26, 2022
Faker is a Python package that generates fake data for you.

Faker is a Python package that generates fake data for you. Whether you need to bootstrap your database, create good-looking XML documents, fill-in yo

Daniele Faraglia 15.2k Jan 01, 2023
A Minimalistic Modern Django Boilerplate

A Minimalistic Modern Django Boilerplate This boilerplate is mainly for educational purposes. It is meant to be cloned as a starter code for future tu

Jonathan Adly 21 Nov 02, 2022
A handy tool for generating Django-based backend projects without coding. On the other hand, it is a code generator of the Django framework.

Django Sage Painless The django-sage-painless is a valuable package based on Django Web Framework & Django Rest Framework for high-level and rapid web

sageteam 51 Sep 15, 2022
Organize Django settings into multiple files and directories. Easily override and modify settings. Use wildcards and optional settings files.

Organize Django settings into multiple files and directories. Easily override and modify settings. Use wildcards in settings file paths and mark setti

Nikita Sobolev 940 Jan 03, 2023
A better and faster multiple selection widget with suggestions

django-searchable-select A better and faster multiple selection widget with suggestions for Django This project is looking for maintainers! Please ope

Andrew Dunai 105 Oct 22, 2022
A Django app that allows visitors to interact with your site as a guest user without requiring registration.

django-guest-user A Django app that allows visitors to interact with your site as a guest user without requiring registration. Largely inspired by dja

Julian Wachholz 21 Dec 17, 2022
A starter template for building a backend with Django and django-rest-framework using docker with PostgreSQL as the primary DB.

Django-Rest-Template! This is a basic starter template for a backend project with Django as the server and PostgreSQL as the database. About the templ

Akshat Sharma 11 Dec 06, 2022
Show how the redis works with Python (Django).

Redis Leaderboard Python (Django) Show how the redis works with Python (Django). Try it out deploying on Heroku (See notes: How to run on Google Cloud

Tom Xu 4 Nov 16, 2021
A UUIDField for Django

django-uuidfield Provides a UUIDField for your Django models. Installation Install it with pip (or easy_install): pip install django-uuidfield Usage

David Cramer 265 Nov 30, 2022
A clone of https://virgool.io written in django

Virgool clone A clone of virgool blog written in django Installation first rename the .env.sample to .env and fill it. with docker docker-compose up -

Danial Selmipoor 7 Dec 23, 2022
Wrap the Blockchain API in Django!

django-blockchain Wrap the Blockchain API in Django. Installation pip install django-blockchain Add app in your settings.py INSTALLED_APPS = [ "d

Dmitry Kalinin 2 Feb 04, 2022
Official clone of the Subversion repository.

Django is a high-level Python Web framework that encourages rapid development and clean, pragmatic design. All documentation is in the "docs" directo

Raymond Penners 3 May 06, 2022
Django API that scrapes and provides the last news of the city of Carlos Casares by semantic way (RDF format).

"Casares News" API Api that scrapes and provides the last news of the city of Carlos Casares by semantic way (RDF format). Usage Consume the articles

Andrés Milla 6 May 12, 2022
Comprehensive Markdown plugin built for Django

Django MarkdownX Django MarkdownX is a comprehensive Markdown plugin built for Django, the renowned high-level Python web framework, with flexibility,

neutronX 738 Dec 21, 2022
Python port of Google's libphonenumber

phonenumbers Python Library This is a Python port of Google's libphonenumber library It supports Python 2.5-2.7 and Python 3.x (in the same codebase,

David Drysdale 3.1k Jan 08, 2023