From 68acc8a52075e9aa3096a538f7e612a2c866fb17 Mon Sep 17 00:00:00 2001 From: Francisco Vicent Date: Thu, 21 Apr 2022 22:17:51 -0300 Subject: [PATCH 1/2] Implement emphasize-lines directive. --- CHANGES.txt | 1 + docs/manual.rst | 15 +++++++++++++ nikola/plugins/compile/rest/listing.py | 31 ++++++++++++++++++++++++-- nikola/utils.py | 27 ++++++++++++++++++++++ tests/test_utils.py | 20 +++++++++++++++++ 5 files changed, 92 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index d16e3d6c6..b9e5d7cd2 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,7 @@ New in master Features -------- +* Add ``emphasize_lines`` directive to code blocks (Issue #3607) * Gallery index pages support the `status` flag (Issue #3598) * Add ``start_at`` option to youtube directive (Issue #3603) diff --git a/docs/manual.rst b/docs/manual.rst index eb98c6e45..773f411a0 100644 --- a/docs/manual.rst +++ b/docs/manual.rst @@ -2694,6 +2694,21 @@ for ``code`` directive are provided: ``code-block`` and ``sourcecode``: print("Our virtues and our failings are inseparable") +Certain lines might be highlighted via the ``emphasize-lines`` directive: + +.. code:: restructuredtext + + .. code-block:: python + :emphasize-lines: 3,5 + + def some_function(): + interesting = False + print('This line is highlighted.') + print('This one is not...') + print('...but this one is.') + +Line ranges are also supported, such as ``:emphasize-lines: 1-3,5-9,15``. + Listing ~~~~~~~ diff --git a/nikola/plugins/compile/rest/listing.py b/nikola/plugins/compile/rest/listing.py index 88ab0c448..48dbe4c37 100644 --- a/nikola/plugins/compile/rest/listing.py +++ b/nikola/plugins/compile/rest/listing.py @@ -57,7 +57,8 @@ class CodeBlock(Directive): 'name': directives.unchanged, 'number-lines': directives.unchanged, # integer or None 'linenos': directives.unchanged, - 'tab-width': directives.nonnegative_int} + 'tab-width': directives.nonnegative_int, + 'emphasize-lines': directives.unchanged_required} has_content = True def run(self): @@ -103,7 +104,33 @@ def run(self): else: anchor_ref = 'rest_code_' + uuid.uuid4().hex - formatter = utils.NikolaPygmentsHTML(anchor_ref=anchor_ref, classes=classes, linenos=linenos, linenostart=linenostart) + linespec = self.options.get('emphasize-lines') + if linespec: + try: + nlines = len(self.content) + hl_lines = utils.parselinenos(linespec, nlines) + if any(i >= nlines for i in hl_lines): + raise self.error( + 'line number spec is out of range(1-%d): %r' % + (nlines, self.options['emphasize-lines']) + ) + hl_lines = [x + 1 for x in hl_lines if x < nlines] + except ValueError as err: + raise self.error(err) + else: + hl_lines = None + + extra_kwargs = {} + if hl_lines is not None: + extra_kwargs['hl_lines'] = hl_lines + + formatter = utils.NikolaPygmentsHTML( + anchor_ref=anchor_ref, + classes=classes, + linenos=linenos, + linenostart=linenostart, + **extra_kwargs + ) out = pygments.highlight(code, lexer, formatter) node = nodes.raw('', out, format='html') diff --git a/nikola/utils.py b/nikola/utils.py index 3acf18f7d..d6f24ee2e 100644 --- a/nikola/utils.py +++ b/nikola/utils.py @@ -62,6 +62,7 @@ from doit.cmdparse import CmdParse from pkg_resources import resource_filename from nikola.packages.pygments_better_html import BetterHtmlFormatter +from typing import List from unidecode import unidecode # Renames @@ -2007,6 +2008,32 @@ def map_metadata(meta, key, config): meta[meta_key] = hook(meta[meta_key]) +def parselinenos(spec: str, total: int) -> List[int]: + """Parse a line number spec (such as "1,2,4-6") and return a list of + wanted line numbers. + """ + items = list() + parts = spec.split(',') + for part in parts: + try: + begend = part.strip().split('-') + if ['', ''] == begend: + raise ValueError + elif len(begend) == 1: + items.append(int(begend[0]) - 1) + elif len(begend) == 2: + start = int(begend[0] or 1) # left half open (cf. -10) + end = int(begend[1] or max(start, total)) # right half open (cf. 10-) + if start > end: # invalid range (cf. 10-1) + raise ValueError + items.extend(range(start - 1, end)) + else: + raise ValueError + except Exception as exc: + raise ValueError('invalid line number spec: %r' % spec) from exc + return items + + class ClassificationTranslationManager(object): """Keeps track of which classifications could be translated as which others. diff --git a/tests/test_utils.py b/tests/test_utils.py index 19966790c..d92ae9c04 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -21,6 +21,7 @@ get_translation_candidate, write_metadata, bool_from_meta, + parselinenos ) @@ -619,3 +620,22 @@ class FakePost: def __init__(self): metadata_extractors.load_defaults(self, self.metadata_extractors_by) + + +def test_parselinenos(): + assert parselinenos('1,2,3', 10) == [0, 1, 2] + assert parselinenos('4, 5, 6', 10) == [3, 4, 5] + assert parselinenos('-4', 10) == [0, 1, 2, 3] + assert parselinenos('7-9', 10) == [6, 7, 8] + assert parselinenos('7-', 10) == [6, 7, 8, 9] + assert parselinenos('1,7-', 10) == [0, 6, 7, 8, 9] + assert parselinenos('7-7', 10) == [6] + assert parselinenos('11-', 10) == [10] + with pytest.raises(ValueError): + parselinenos('1-2-3', 10) + with pytest.raises(ValueError): + parselinenos('abc-def', 10) + with pytest.raises(ValueError): + parselinenos('-', 10) + with pytest.raises(ValueError): + parselinenos('3-1', 10) From dcf471bc2b8617342dbe0b64410abbd185e105fb Mon Sep 17 00:00:00 2001 From: Chris Warrick Date: Fri, 22 Apr 2022 20:53:06 +0200 Subject: [PATCH 2/2] Improve docstring for parselinenos --- nikola/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nikola/utils.py b/nikola/utils.py index d6f24ee2e..e2a64a3cc 100644 --- a/nikola/utils.py +++ b/nikola/utils.py @@ -2009,8 +2009,9 @@ def map_metadata(meta, key, config): def parselinenos(spec: str, total: int) -> List[int]: - """Parse a line number spec (such as "1,2,4-6") and return a list of - wanted line numbers. + """Parse a line number spec. + + Example: "1,2,4-6" -> [0, 1, 3, 4, 5] """ items = list() parts = spec.split(',')