Skip to content

Commit

Permalink
Merge pull request #3 from spacether/feat_improves_nebulous_tag_handling
Browse files Browse the repository at this point in the history
Feat improves nebulous tag handling
  • Loading branch information
spacether authored Oct 10, 2021
2 parents c7d7d14 + 677df4e commit 9749c08
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 358 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ Handles conversion for if/unless first + last
- {{/-first}} -> {{\if}} OR {{\unless}}
- {{/-last}} -> {{\if}} OR {{\unless}}

Handles adding whitespace characters at the end of tags when
- single tags are used on a template line + the tag is an if or unless tag
Tags that begin with # are ambiguous when going from mustache to handlebars because
i mustache, # handles, truthy input, array input, and object context input.
So in handlebars, a mustache #someTag can be #if someTag, #each someTag, or #with someTag
- this tool prints out those nebulous tags so the user can decide which usage it should be
- one can assign those tags to the if/each/with use cases with the command line arguments
- -handlebars_if_tags
- -handlebars_each_tags
- -handlebars_with_tags

## testing
Install pytest in your virtual environment and then run make test
193 changes: 159 additions & 34 deletions mustache_to_handlebars/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,61 @@
import re
from enum import Enum

TAG_OPEN = '{{'
TAG_CLOSE = '}}'
HANDLEBARS_EXTENSION = 'handlebars'
HANDLEBARS_WHITESPACE_REMOVAL_CHAR = '~'
HANDLEBARS_FIRST = '@first'
HANDLEBARS_LAST = '@last'


MUSTACHE_EXTENSION = 'mustache'
MUSTACHE_IF_UNLESS_CLOSE_PATTERN = r'{{([#^/].+?)}}'
MUSTACHE_TO_HANDLEBARS_TAG = {
'-first': '@first',
'-last': '@last'
'-first': HANDLEBARS_FIRST,
'-last': HANDLEBARS_LAST
}

HANDLEBARS_EXTENSION = 'handlebars'
HANDLEBARS_IF_CLOSE = '{{/if}}'
HANDLEBARS_UNLESS_CLOSE = '{{/unless}}'
HANDLEBARS_WHITESPACE_REMOVAL_CHAR = '~'

class MustacheTagType(str, Enum):
IF = '#' # it is unclear if this should be an if(presence) OR each(list iteration) OR with(enter object context)
IF_EACH_WITH = '#' # it is unclear if this should be an if(presence) OR each(list iteration) OR with(enter object context)
UNLESS = '^'
CLOSE = '/'

def mustache_to_handlebars_tag_element(mustache_tag_element: str) -> str:

class HandlebarsTagType(Enum):
# value is open prefix, close tag
IF = ('#if', '/if')
EACH = ('#each', '/each')
WITH = ('#with', '/with')
UNLESS = ('#unless', '/unless')
CLOSE = ('', '')


def __get_handlebars_tag_type(
tag: str,
mustache_tag_control_character: str,
handlebars_if_tags: typing.Set[str],
handlebars_each_tags: typing.Set[str],
handlebars_with_tags: typing.Set[str]
) -> typing.Optional[HandlebarsTagType]:
# mustache_tag_control_character: the #/^ control character
mustache_tag_type = MustacheTagType(mustache_tag_control_character)
if mustache_tag_type is MustacheTagType.IF_EACH_WITH:
if tag in handlebars_if_tags:
return HandlebarsTagType.IF
if tag in handlebars_each_tags:
return HandlebarsTagType.EACH
if tag in handlebars_with_tags:
return HandlebarsTagType.WITH
elif mustache_tag_type is MustacheTagType.UNLESS:
return HandlebarsTagType.UNLESS
elif mustache_tag_type is MustacheTagType.CLOSE:
return HandlebarsTagType.CLOSE
return None


def __mustache_to_handlebars_tag_element(mustache_tag_element: str) -> str:
"""
'-first' -> '@first'
Input excludes the #/^ control characters
Expand All @@ -33,20 +69,32 @@ def mustache_to_handlebars_tag_element(mustache_tag_element: str) -> str:
return mustache_tag_element
return handlebars_tag_element

def __dir_path(string):
if os.path.isdir(string):
return string

def __dir_path(path: str) -> str:
if os.path.isdir(path):
return path
else:
raise NotADirectoryError(string)
raise NotADirectoryError(path)


def __list_of_strings(space_delim_tags: str) -> typing.Set[str]:
if not isinstance(space_delim_tags, str):
raise ValueError('Invalid type, a space delimited string must be passed in')
return set(space_delim_tags.split(' '))


def __get_args():
parser = argparse.ArgumentParser(description='convert templates from mustache to handebars')
parser.add_argument('in_dir', type=__dir_path)
parser.add_argument('-out_dir', type=str)
parser.add_argument('-handlebars_if_tags', type=__list_of_strings, default=set())
parser.add_argument('-handlebars_each_tags', type=__list_of_strings, default=set())
parser.add_argument('-handlebars_with_tags', type=__list_of_strings, default=set())
parser.add_argument('-recursive', type=bool, default=True)
parser.add_argument('-delete_in_files', type=bool, default=False)
args = parser.parse_args()
return args.in_dir, args.out_dir, args.recursive, args.delete_in_files
return args


def _get_in_file_to_out_file_map(in_dir: str, out_dir: str, recursive: bool) -> dict:
path_args = []
Expand Down Expand Up @@ -93,16 +141,23 @@ def _add_whitespace_handling(in_txt: str) -> str:
pass
return '\n'.join(lines)

def _convert_handlebars_to_mustache(in_txt: str) -> str:

def _convert_handlebars_to_mustache(
in_txt: str,
handlebars_if_tags: typing.Set[str],
handlebars_each_tags: typing.Set[str],
handlebars_with_tags: typing.Set[str]
) -> typing.Tuple[str, typing.Set[str]]:
# extract all control tags from {{#someTag}} and {{/someTag}} patterns
tags = set(re.findall(MUSTACHE_IF_UNLESS_CLOSE_PATTERN, in_txt))
ambiguous_tags = set()
if not tags:
return in_txt
return in_txt, ambiguous_tags
replacement_index_to_from_to_pair = {}
closures = []
for i in range(len(in_txt)):
for tag_without_braces in tags:
tag_with_braces = '{{'+tag_without_braces+'}}'
tag_with_braces = TAG_OPEN + tag_without_braces + TAG_CLOSE

end_index = i + len(tag_with_braces)
if end_index > len(in_txt):
Expand All @@ -111,37 +166,59 @@ def _convert_handlebars_to_mustache(in_txt: str) -> str:

if substr == tag_with_braces:
tag = tag_without_braces[1:]
tag = mustache_to_handlebars_tag_element(tag)
tag_type = MustacheTagType(tag_without_braces[0])

if tag_type is MustacheTagType.IF:
new_tag = '{{#if ' + tag + '}}'
closures.append(HANDLEBARS_IF_CLOSE)
elif tag_type is MustacheTagType.UNLESS:
new_tag = '{{#unless ' + tag + '}}'
closures.append(HANDLEBARS_UNLESS_CLOSE)
elif tag_type is MustacheTagType.CLOSE:
new_tag = closures.pop()
tag = __mustache_to_handlebars_tag_element(tag)
handlebars_tag_type = __get_handlebars_tag_type(
tag,
tag_without_braces[0],
handlebars_if_tags,
handlebars_each_tags,
handlebars_with_tags
)

if (
handlebars_tag_type in
{HandlebarsTagType.IF, HandlebarsTagType.EACH, HandlebarsTagType.WITH, HandlebarsTagType.UNLESS}
):
new_tag = TAG_OPEN + handlebars_tag_type.value[0] + ' ' + tag + TAG_CLOSE
closures.append(handlebars_tag_type.value[1])
elif handlebars_tag_type is None:
ambiguous_tags.add(tag)
new_tag = '{{#ifOrEachOrWith ' + tag + TAG_CLOSE
closures.append('/ifOrEachOrWith')
elif handlebars_tag_type is HandlebarsTagType.CLOSE:
new_tag = TAG_OPEN + closures.pop() + TAG_CLOSE

replacement_index_to_from_to_pair[i] = (tag_with_braces, new_tag)
break


out_txt = str(in_txt)
for i, (original_tag, new_tag) in reversed(replacement_index_to_from_to_pair.items()):
out_txt = out_txt[0:i] + new_tag + out_txt[i+len(original_tag):]

out_txt = _add_whitespace_handling(out_txt)
return out_txt
return out_txt, ambiguous_tags

def _create_files(in_path_to_out_path: dict):

def _create_files(
in_path_to_out_path: dict,
handlebars_if_tags: typing.Set[str],
handlebars_each_tags: typing.Set[str],
handlebars_with_tags: typing.Set[str]
) -> typing.Tuple[typing.List[str], typing.Set[str]]:
existing_out_folders = set()
ambiguous_tags = set()
input_files_used_to_make_output_files = []
for i, (in_path, out_path) in enumerate(in_path_to_out_path.items()):
print('Reading file {} out of {}, path={}'.format(i+1, len(in_path_to_out_path), in_path))
with open(in_path) as file:
in_txt = file.read()

out_txt = _convert_handlebars_to_mustache(in_txt)
out_txt, file_ambiguous_tags = _convert_handlebars_to_mustache(in_txt, handlebars_if_tags, handlebars_each_tags, handlebars_with_tags)
if file_ambiguous_tags:
ambiguous_tags.update(file_ambiguous_tags)
print('Skipped writing file {} because it has ambiguous tags'.format(out_path))
continue

out_folder = os.path.dirname(out_path)
if out_folder not in existing_out_folders:
if not os.path.isdir(out_folder):
Expand All @@ -150,7 +227,10 @@ def _create_files(in_path_to_out_path: dict):

with open(out_path, 'w') as file:
file.write(out_txt)
input_files_used_to_make_output_files.append(in_path)
print('Wrote file {}'.format(out_path))
return input_files_used_to_make_output_files, ambiguous_tags


def _clean_up_files(files_to_delete: typing.List[str]):
if not files_to_delete:
Expand All @@ -161,12 +241,57 @@ def _clean_up_files(files_to_delete: typing.List[str]):
os.remove(path)


def __handle_ambiguous_tags(ambiguous_tags: typing.Set[str], qty_skipped_files: int):
print('\nskipped generating {} files'.format(qty_skipped_files))
print('qty_ambiguous_tags={}'.format(len(ambiguous_tags)))
print('ambiguous_tags={}'.format(ambiguous_tags))

suspected_if_tags = []
suspected_each_tags = []
for tag in ambiguous_tags:
if (
tag.startswith('is') or
tag.startswith('has') or
tag.startswith('getHas') or
tag.startswith('use') or
tag.startswith('getIs')
):
suspected_if_tags.append(tag)
ambiguous_tags = ambiguous_tags - set(suspected_if_tags)
for tag in ambiguous_tags:
if tag.endswith('s'):
suspected_each_tags.append(tag)
ambiguous_tags = ambiguous_tags - set(suspected_each_tags)
suspected_with_tags = list(ambiguous_tags)
print('here are some guesses at what your tags may be based only on tag names:\n')
print('-handlebars_if_tags=\"{}\"\n'.format(" ".join(suspected_if_tags)))
print('-handlebars_each_tags=\"{}\"\n'.format(" ".join(suspected_each_tags)))
print('-handlebars_with_tags=\"{}\"\n'.format(" ".join(suspected_with_tags)))


def mustache_to_handlebars():
in_dir, out_dir, recursive, delete_in_files = __get_args()
args = __get_args()
in_dir, out_dir, recursive, delete_in_files = (args.in_dir, args.out_dir, args.recursive, args.delete_in_files)
handlebars_if_tags, handlebars_each_tags, handlebars_with_tags = (
args.handlebars_if_tags, args.handlebars_each_tags, args.handlebars_with_tags)
handlebars_if_tags.update({HANDLEBARS_FIRST, HANDLEBARS_LAST})

if not out_dir:
out_dir = in_dir

in_path_to_out_path = _get_in_file_to_out_file_map(in_dir, out_dir, recursive)
_create_files(in_path_to_out_path)
input_files_used_to_make_output_files, ambiguous_tags = _create_files(
in_path_to_out_path,
handlebars_if_tags,
handlebars_each_tags,
handlebars_with_tags,
)

if ambiguous_tags:
__handle_ambiguous_tags(
ambiguous_tags,
len(in_path_to_out_path) - len(input_files_used_to_make_output_files)
)

if delete_in_files:
_clean_up_files(list(in_path_to_out_path.keys()))
_clean_up_files(input_files_used_to_make_output_files)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from distutils.core import setup

setup(name='mustache_to_handlebars',
version='0.91',
version='0.92',
description='converts mustache to handlebars templates',
author='Justin Black',
url='https://github.com/spacether/mustache_to_handlebars',
Expand Down
Loading

0 comments on commit 9749c08

Please sign in to comment.