diff --git a/docs/features/admonition/backquotes.md b/docs/features/admonition/backquotes.md index 0c96907..8545dd1 100644 --- a/docs/features/admonition/backquotes.md +++ b/docs/features/admonition/backquotes.md @@ -12,32 +12,38 @@ title: Admonition Backquotes ### Admonition Backquotes -=== "obsidian markdown" -~~~ + +~~~tabs +---tab obsidian markdown +```` ```ad-tip title: This is a tip This is the content of the admonition tip. ``` +```` +---tab obsidian rendered +![[images/backquotes_1.png]] ~~~ -=== "obsidian rendered" -![[images/backquotes_1.png]] -### mkdocs-material admonition -=== "mkdocs-material markdown" + +### mkdocs-material admonition + +~~~tabs +---tab mkdocs-material markdown ``` !!!tip "This is a tip" This is the content of the admonition tip. ``` -=== "mkdocs-material rendered" - +---tab mkdocs-material rendered ```ad-tip title: This is a tip This is the content of the admonition tip. -``` \ No newline at end of file +``` +~~~ diff --git a/docs/features/comment/index.md b/docs/features/comment/index.md index 9c74325..047ea84 100644 --- a/docs/features/comment/index.md +++ b/docs/features/comment/index.md @@ -11,41 +11,39 @@ title: Comment ## Usage ### obsidian comment - -=== "obsidian markdown" - -``` +```tabs +---tab obsidian markdown +~~~ This is an %%inline%% comment. %% This is a block comment. Block comments can span multiple lines. %% -``` - -=== "obsidian reading view" +~~~ +---tab obsidian reading view ![[images/comment_1.png]] +``` -### mkdocs-material comment -=== "mkdocs-material markdown" +### mkdocs-material comment -``` +```tabs +---tab mkdocs-material markdown +~~~ This is an comment. -``` - -=== "mkdocs-material rendered" +~~~ +---tab mkdocs-material rendered This is an %%inline%% comment. - %% This is a block comment. Block comments can span multiple lines. %% - +``` diff --git a/docs/features/tabs/index.md b/docs/features/tabs/index.md index 669fa6c..fa54c44 100644 --- a/docs/features/tabs/index.md +++ b/docs/features/tabs/index.md @@ -50,6 +50,8 @@ This tab contains callout ### mkdocs-material content tabs +**mkdocs markdown** + ~~~~ === "First *tab*" @@ -88,6 +90,8 @@ This tab contains callout ~~~~ +**rendered** + ```tabs ---tab First *tab* This is a *sample* **tab** with some markdown : diff --git a/obsidian_support/conversion/abstract_conversion.py b/obsidian_support/conversion/abstract_conversion.py index 009beec..ec7e02a 100644 --- a/obsidian_support/conversion/abstract_conversion.py +++ b/obsidian_support/conversion/abstract_conversion.py @@ -1,11 +1,11 @@ -import re from abc import * -from enum import Enum -from typing import List, Tuple +from typing import List from mkdocs.structure.pages import Page from overrides import final +from obsidian_support.conversion.util import is_overlapped, get_exclude_indices + """ An abstract class that implies a conversion from `obsidian syntax` to `mkdocs-material syntax` @@ -20,32 +20,6 @@ SyntaxGroup = List[str] -class MarkdownPatterns(Enum): - # Exclude Patterns - """ regex that matches markdown `tilde code block` (triple tilde syntax)""" - TILDE_CODE_BLOCK = r"([A-Za-z \t]*)~~~([-A-Za-z]*)?\n([\s\S]*?)~~~([A-Za-z \t]*)*" - - """regex that matches markdown `backtick code block` (triple backtick syntax)""" - BACKTICK_CODE_BLOCK = r"([A-Za-z \t]*)```([-A-Za-z]*)?\n([\s\S]*?)```([A-Za-z \t]*)*" - - """regex that matches markdown code (single backtick syntax)""" - BACKTICK_CODE = r"`[\S\s]*?`" - - # Do Not Exclude Patterns - """regex that matches markdown `admonition backquotes` (triple tilde syntax)""" - ADMONITION_BACKQUOTES_CODE_BLOCK = r"([A-Za-z \t]*)```ad-([-A-Za-z]*)?\n([\s\S]*?)```([A-Za-z \t]*)*" - - """regex that matches markdown `tabs backquotes code block` (triple tilde syntax)""" - TABS_BACKQUOTES_CODE_BLOCK = r"([A-Za-z \t]*)```tabs([-A-Za-z]*)?\n([\s\S]*?)```([A-Za-z \t]*)*" - - """regex that matches markdown `tabs tilde code block` (triple tilde syntax)""" - TABS_TILDE_CODE_BLOCK = r"([A-Za-z \t]*)~~~tabs([-A-Za-z]*)?\n([\s\S]*?)~~~([A-Za-z \t]*)*" - - @property - def regex(self): - return self.value - - class AbstractConversion(metaclass=ABCMeta): @property @@ -69,16 +43,15 @@ def convert(self, syntax_groups: SyntaxGroup, page: Page) -> str: def markdown_convert(self, markdown: str, page: Page) -> str: converted_markdown = "" index = 0 - excluded_indices = self._get_excluded_indices(markdown) - excluded_indices = self._filter_do_not_exclude_indices(excluded_indices, markdown) + excluded_indices = get_exclude_indices(markdown) for obsidian_syntax in self.obsidian_regex_pattern.finditer(markdown): - ## found range of markdown where the obsidian_regex matches + # find range of markdown where the obsidian_regex matches start = obsidian_syntax.start() end = obsidian_syntax.end() - 1 - ## continue if match is in excluded range - if self._is_overlapped(start, end, excluded_indices): + # continue if match is in excluded range + if is_overlapped(start, end, excluded_indices): continue syntax_groups = list(map(lambda group: obsidian_syntax.group(group), self.obsidian_regex_groups)) @@ -90,46 +63,3 @@ def markdown_convert(self, markdown: str, page: Page) -> str: converted_markdown += markdown[index:len(markdown)] return converted_markdown - - @staticmethod - def _get_excluded_indices(markdown: str) -> List[tuple]: - tilde_code_block_indices = [] - for code in re.finditer(MarkdownPatterns.TILDE_CODE_BLOCK.regex, markdown): - tilde_code_block_indices.append((code.start(), code.end() - 1)) - - backtick_code_block_indices = [] - for code in re.finditer(MarkdownPatterns.BACKTICK_CODE_BLOCK.regex, markdown): - if not AbstractConversion._is_overlapped(code.start(), code.end() - 1, tilde_code_block_indices): - backtick_code_block_indices.append((code.start(), code.end() - 1)) - - backtick_code_indices = [] - for code in re.finditer(MarkdownPatterns.BACKTICK_CODE.regex, markdown): - if not AbstractConversion._is_overlapped(code.start(), code.end() - 1, tilde_code_block_indices) and \ - not AbstractConversion._is_overlapped(code.start(), code.end() - 1, backtick_code_block_indices): - backtick_code_indices.append((code.start(), code.end() - 1)) - - return tilde_code_block_indices + backtick_code_block_indices + backtick_code_indices - - @staticmethod - def _filter_do_not_exclude_indices(exclude_indices: List[Tuple], markdown) -> List[Tuple]: - admonition_backquotes_code_block_indices = [] - for code in re.finditer(MarkdownPatterns.ADMONITION_BACKQUOTES_CODE_BLOCK.regex, markdown): - admonition_backquotes_code_block_indices.append((code.start(), code.end() - 1)) - - tabs_backquotes_code_block_indices = [] - for code in re.finditer(MarkdownPatterns.TABS_BACKQUOTES_CODE_BLOCK.regex, markdown): - tabs_backquotes_code_block_indices.append((code.start(), code.end() - 1)) - - tabs_tilde_code_block_indices = [] - for code in re.finditer(MarkdownPatterns.TABS_TILDE_CODE_BLOCK.regex, markdown): - tabs_tilde_code_block_indices.append((code.start(), code.end() - 1)) - - do_not_exclude_indices = admonition_backquotes_code_block_indices + tabs_backquotes_code_block_indices + tabs_tilde_code_block_indices - return list(filter(lambda indices: indices not in do_not_exclude_indices, exclude_indices)) - - @staticmethod - def _is_overlapped(start: int, end: int, exclude_indices_pairs: List[tuple]) -> bool: - for exclude_indices_pair in exclude_indices_pairs: - if exclude_indices_pair[0] <= start and end <= exclude_indices_pair[1]: - return True - return False diff --git a/obsidian_support/conversion/util.py b/obsidian_support/conversion/util.py new file mode 100644 index 0000000..dc16a63 --- /dev/null +++ b/obsidian_support/conversion/util.py @@ -0,0 +1,79 @@ +import dataclasses +import re +from typing import Tuple, List + +CODE_BLOCK_PATTERN = re.compile(r'(?P^[`~]{3,})(?P[a-zA-Z\-]*)?', re.MULTILINE) +BACKQUOTES_CODE_PATTERN = re.compile(r"`[^\n`]+`") + + +@dataclasses.dataclass +class CodeBlockSyntax: + start: int + end: int + code_block_type: str + language: str + + +def get_exclude_indices(markdown: str) -> List[Tuple[int, int]]: + # setup + exclude_indices = [] + code_block_matches = {} + + # step 1 + # get all lines stars with more than three of ` or ~ + # and group it by its size + for code_block_match in CODE_BLOCK_PATTERN.finditer(markdown): + code_block_syntax = code_block_match.group("code_block") + size = len(code_block_syntax) + code_block_type = code_block_syntax[0] + start = code_block_match.start() + end = code_block_match.end() + language = code_block_match.group("language") + + if size not in code_block_matches: + code_block_matches[size] = [] + code_block_matches[size].append(CodeBlockSyntax(start, end, code_block_type, language)) + + # step 2 + # loop the code_block_matches in desc sorted by its size + for code_block_size in sorted(code_block_matches.keys(), reverse=True): + code_block_matches_with_same_size = code_block_matches[code_block_size] + current_syntax = None + nested_code_block_syntax = None + + # filter already excluded code_block_matches + code_block_matches_with_same_size = [it for it in code_block_matches_with_same_size if + not is_overlapped(it.start, it.end, exclude_indices)] + for code_block_syntax in code_block_matches_with_same_size: + if current_syntax is None: + current_syntax = code_block_syntax + elif current_syntax.code_block_type == code_block_syntax.code_block_type and \ + not is_overlapped(current_syntax.start, code_block_syntax.end, exclude_indices): + # do not exclude if its ad or tabs block + if not current_syntax.language.startswith("ad-") and not current_syntax.language == "tabs": + exclude_indices.append((current_syntax.start, code_block_syntax.end)) + current_syntax = None + elif (current_syntax.language.startswith("ad-") or current_syntax.language == "tabs") and \ + current_syntax.code_block_type != code_block_syntax.code_block_type and \ + nested_code_block_syntax is None: + nested_code_block_syntax = code_block_syntax + elif nested_code_block_syntax is not None and \ + code_block_syntax.code_block_type == nested_code_block_syntax.code_block_type: + if not nested_code_block_syntax.language.startswith("ad-") and not nested_code_block_syntax.language == "tabs": + exclude_indices.append((nested_code_block_syntax.start, code_block_syntax.end)) + nested_code_block_syntax = None + + # step 3 + # exclude backquotes_codes (``) + for code_match in re.finditer(BACKQUOTES_CODE_PATTERN, markdown): + if not is_overlapped(code_match.start(), code_match.end(), exclude_indices): + exclude_indices.append((code_match.start(), code_match.end())) + + return exclude_indices + + +def is_overlapped(start: int, end: int, exclude_indices_pairs: List[tuple]) -> bool: + for exclude_indices_pair in exclude_indices_pairs: + if exclude_indices_pair[0] <= start and end <= exclude_indices_pair[1]: + return True + return False diff --git a/obsidian_support/plugin.py b/obsidian_support/plugin.py index 67c7c94..4c3fad2 100644 --- a/obsidian_support/plugin.py +++ b/obsidian_support/plugin.py @@ -32,13 +32,13 @@ def __init__(self): def on_page_markdown(self, markdown, page, config, files): # apply conversions - markdown = self.admonition_callout_conversions.markdown_convert(markdown, page) - markdown = self.tabs_backquotes_conversion.markdown_convert(markdown, page) - markdown = self.tabs_tilde_block_conversion.markdown_convert(markdown, page) markdown = self.admonition_backquotes_conversion.markdown_convert(markdown, page) markdown = self.comment_conversion.markdown_convert(markdown, page) markdown = self.pdf_conversion.markdown_convert(markdown, page) markdown = self.image_web_link_conversions.markdown_convert(markdown, page) markdown = self.image_internal_link_conversion.markdown_convert(markdown, page) markdown = self.tags_conversion.markdown_convert(markdown, page) + markdown = self.admonition_callout_conversions.markdown_convert(markdown, page) + markdown = self.tabs_tilde_block_conversion.markdown_convert(markdown, page) + markdown = self.tabs_backquotes_conversion.markdown_convert(markdown, page) return markdown diff --git a/setup.py b/setup.py index d23ba68..46b0515 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -VERSION_NUMBER = '1.3.0' +VERSION_NUMBER = '1.3.1' def read_file(fname): diff --git a/test/conversion/test_abstract_conversion.py b/test/conversion/test_abstract_conversion.py index 5f8b30c..1848dc1 100644 --- a/test/conversion/test_abstract_conversion.py +++ b/test/conversion/test_abstract_conversion.py @@ -1,56 +1,7 @@ +from inspect import cleandoc from typing import List from assertpy import assert_that -from obsidian_support.conversion.abstract_conversion import AbstractConversion +from obsidian_support.conversion.util import get_exclude_indices - -def test_get_code_indices(): - # given - markdown = """## Hi there. -this is test `code` - -### Kotlin -```kotlin -fun main() { - println("Hello world!") -} -``` - -### Tip -```ad-tip -some tip -``` - -### Tilde -~~~markdown -some tip -~~~ - -### Nested -~~~markdown -```kotlin -fun main() { - println("Hello world!") -} -``` -some tip -`some code` -~~~""" - - # when - code_indices: List[tuple] = AbstractConversion._get_excluded_indices(markdown) - - # then - assert_that(code_indices).is_length(5) - - # tilde_code_block_indices - assert_that(code_indices[0]).is_equal_to((145, 168)) # indices of `~~~markdown\n somde tip\n~~~` - assert_that(code_indices[1]).is_equal_to((182, 274)) # indices of `~~~markdown\n ```kotlin\n ... ~~~` - - # backtick_code_block_indices - assert_that(code_indices[2]).is_equal_to((45, 100)) # indices of ````kotlin\n fun ...```` - assert_that(code_indices[3]).is_equal_to((111, 132)) # indices of ````ad-tip\nsomde tip\n```` - - # backtick_code_indices - assert_that(code_indices[4]).is_equal_to((26, 31)) # indices of ``code`` diff --git a/test/conversion/test_util.py b/test/conversion/test_util.py new file mode 100644 index 0000000..1cb8d76 --- /dev/null +++ b/test/conversion/test_util.py @@ -0,0 +1,204 @@ +from inspect import cleandoc +from typing import List + +from assertpy import assert_that + +from obsidian_support.conversion.util import get_exclude_indices + + +def test_get_exclude_indices_1(): + # given + markdown = cleandoc(""" + `exclude me` + """) + + # when + exclude_indices = get_exclude_indices(markdown) + + # then + assert_that(exclude_indices).is_length(1) + assert_that(exclude_indices[0]).is_equal_to((0, 12)) + + +def test_get_exclude_indices_2(): + # given + markdown = cleandoc(""" + ```tabs + ---tab obsidian markdown + ~~~ + ![[features/pdf/a4-sample.pdf]] + ~~~ + + ~~~ + ![[features/pdf/a5-sample.pdf]] + ~~~ + + You can also specify the height in pixels for the embedded PDF viewer, by adding `#height=[number]` to the link. like this : `![[features/pdf/a4-sample.pdf#height=400]]` + + ---tab obsidian rendered + ![[images/pdf_1.png]] + ``` + """) + + # when + exclude_indices = get_exclude_indices(markdown) + + # then + print(exclude_indices) + + +def test_get_exclude_indices_3(): + # given + markdown = cleandoc(""" + ## Hi there. + this is test `code` + + ### Kotlin + ```kotlin + fun main() { + println("Hello world!") + } + ``` + + ### Tip + ```ad-tip + some tip + ``` + + ### Tilde + ~~~markdown + some tip + ~~~ + + ### Nested + ~~~markdown + ```kotlin + fun main() { + println("Hello world!") + } + ``` + some tip + `some code` + ~~~ + """) + + # when + code_indices: List[tuple] = get_exclude_indices(markdown) + + # then + assert_that(code_indices).is_length(4) + + assert_that(code_indices[0]).is_equal_to((45, 101)) # indices of ````kotlin\n fun ...```` + assert_that(code_indices[1]).is_equal_to((145, 169)) # indices of `~~~markdown\n somde tip\n~~~` + assert_that(code_indices[2]).is_equal_to((182, 275)) # indices of `~~~markdown\n ```kotlin\n ... ~~~` + assert_that(code_indices[3]).is_equal_to((26, 32)) # indices of ``code`` + + +def test_get_exclude_indices_4(): + # given + markdown = cleandoc(""" + ~~~~ + ```tabs + ---tab First *tab* + This is a *sample* **tab** with some markdown : + ## Heading 2 + - [ ] Task 1 + - [ ] Task 2 + + ### Heading 3 + * Sed sagittis eleifend rutrum + * Donec vitae suscipit est + * Nulla tempor lobortis orci + + ---tab Second tab + This tab has a code block: + ~~~cpp + include + using namespace std; + + int main() { + char c; + cout << "Enter a character: "; + cin >> c; + cout << "ASCII Value of " << c << " is " << int(c); + return 0; + } + ~~~ + ---tab Last tab + This tab contains callout + > [!note] tab with callout! + > callout content + ``` + ~~~~ + """) + + # when + code_indices: List[tuple] = get_exclude_indices(markdown) + + # then + assert_that(code_indices).is_length(1) + assert_that(code_indices[0]).is_equal_to((0, 546)) # indices of `~~~~\n...~~~~` + + +def test_get_exclude_indices_5(): + # given + markdown = cleandoc(""" + `exclude me` + + ``` + exclude me + ``` + + ````` + exclude me + ````` + + ~~~ + exclude me + ~~~ + + ~~~~~ + exclude me + ~~~~~ + + ````` + ~~~ exclude me + ````` + + ```ad-tip + include me + ``` + + ```kotlin + exclude me + ``` + + ```tabs + ---tab include me + ``` + + ~~~~ + ```tabs + ---tab exclude me + ~~~ + exclude me + ~~~ + ``` + ~~~~ + + ```` + ~~~tabs + ---tab exclude me + ``` + exclude me + ``` + ~~~ + ```` + + """) + + # when + exclude_indices = get_exclude_indices(markdown) + + # then + print(exclude_indices)