diff --git a/scripts/bme_relatives.py b/scripts/bme_relatives.py deleted file mode 100644 index 46ae874..0000000 --- a/scripts/bme_relatives.py +++ /dev/null @@ -1,433 +0,0 @@ -import typing -import simple_po, bme_utils - -#region Translation Constant - -## TODO: -# This translation context string prefix is cpoied from UTIL_translation.py. -# If the context string of translation changed, please synchronize it. - -CTX_TRANSLATION: str = 'BBP/BME' - -#endregion - -#region BME Tokens - -## TODO: -# These token are copied from UTIL_bme.py. -# If anything changed, such as BME standard, these tokens should be synchronized between these 2 modules. - -TOKEN_IDENTIFIER: str = 'identifier' - -TOKEN_SHOWCASE: str = 'showcase' -TOKEN_SHOWCASE_TITLE: str = 'title' -TOKEN_SHOWCASE_ICON: str = 'icon' -TOKEN_SHOWCASE_TYPE: str = 'type' -TOKEN_SHOWCASE_CFGS: str = 'cfgs' -TOKEN_SHOWCASE_CFGS_FIELD: str = 'field' -TOKEN_SHOWCASE_CFGS_TYPE: str = 'type' -TOKEN_SHOWCASE_CFGS_TITLE: str = 'title' -TOKEN_SHOWCASE_CFGS_DESC: str = 'desc' -TOKEN_SHOWCASE_CFGS_DEFAULT: str = 'default' - -TOKEN_SKIP: str = 'skip' - -TOKEN_PARAMS: str = 'params' -TOKEN_PARAMS_FIELD: str = 'field' -TOKEN_PARAMS_DATA: str = 'data' - -TOKEN_VARS: str = 'vars' -TOKEN_VARS_FIELD: str = 'field' -TOKEN_VARS_DATA: str = 'data' - -TOKEN_VERTICES: str = 'vertices' -TOKEN_VERTICES_SKIP: str = 'skip' -TOKEN_VERTICES_DATA: str = 'data' - -TOKEN_FACES: str = 'faces' -TOKEN_FACES_SKIP: str = 'skip' -TOKEN_FACES_TEXTURE: str = 'texture' -TOKEN_FACES_INDICES: str = 'indices' -TOKEN_FACES_UVS: str = 'uvs' -TOKEN_FACES_NORMALS: str = 'normals' - -TOKEN_INSTANCES: str = 'instances' -TOKEN_INSTANCES_IDENTIFIER: str = 'identifier' -TOKEN_INSTANCES_SKIP: str = 'skip' -TOKEN_INSTANCES_PARAMS: str = 'params' -TOKEN_INSTANCES_TRANSFORM: str = 'transform' - -#endregion - -# TODO: finish BME validator - -# class ReporterWithHierarchy(): -# """ -# BME validator and extractor specifically used reporter -# which auotmatically use hierarchy as its context when outputing. -# """ - -# __mReporter: bme_utils.Reporter -# __mHierarchy: bme_utils.Hierarchy - -# def __init__(self, reporter: bme_utils.Reporter, hierarchy: bme_utils.Hierarchy): -# self.__mReporter = reporter -# self.__mHierarchy = hierarchy - -# def error(self, msg: str) -> None: -# self.__mReporter.error(msg, self.__mHierarchy.build_hierarchy_string()) -# def warning(self, msg: str) -> None: -# self.__mReporter.warning(msg, self.__mHierarchy.build_hierarchy_string()) -# def info(self, msg: str) -> None: -# self.__mReporter.info(msg, self.__mHierarchy.build_hierarchy_string()) - -# class UniqueField(): -# """ -# Some BME prototype fields should be unique in globl scope. -# So BME validator should check this. That's the feature this class provided. - -# This class is an abstract class and should not be used directly. -# Use child class please. -# """ - -# __mUniques: set[str] -# __mReporter: ReporterWithHierarchy - -# def __init__(self, reporter: ReporterWithHierarchy): -# self.__mUniques = set() -# self.__mReporter = reporter - -# def register(self, entry: str) -> bool: -# """ -# @brief Try to register given entry in unique. -# @details -# If given entry is not presented in unique set, given entry will be inserted and return True. -# If given entry is already available in unique set, this function will use reporter to output an error message and return False. -# @param[in] entry The entry to be checked and inserted. -# @return True if entry is unique, otherwise false. -# """ -# if entry in self.__mUniques: -# self.__mReporter.error(self._get_error_msg(entry)) -# return False -# else: -# self.__mUniques.add(entry) -# return True - -# def clear(self) -> None: -# """ -# @brief Clear this unique set for further using. -# """ -# self.__mUniques.clear() - -# def _get_error_msg(self, err_entry: str) -> str: -# """ -# @brief Get the error message when error occurs. -# @details -# This is internal used function to get the error message which will be passed to reporter. -# This message is generated by given entry which cause the non-unique issue. -# Outer caller should not call this function and every child class should override this function. -# @param[in] err_entry The entry cause the error. -# @return The error message generated from given error entry. -# """ -# raise NotImplementedError() - -# class UniqueIdentifier(UniqueField): -# """Specific UniqueField for unique prototype identifier.""" -# def _get_error_msg(self, err_entry: str) -> str: -# return f'Trying to register multiple prototype with same name: "{err_entry}".' -# class UniqueVariable(UniqueField): -# """Specific UniqueField for unique variable names within prototype.""" -# def _get_error_msg(self, err_entry: str) -> str: -# return f'Trying to define multiple variable with same name: "{err_entry}" in the same prototype.' - -# class BMEValidator(): -# """ -# The validator for BME prototype declarartions. -# This validator will validate given prototype declaration JSON structure, -# to check then whether have all essential fields BME standard required and whether have any unknown fields. -# """ - -# __mHierarchy: bme_utils.Hierarchy -# __mReporter: ReporterWithHierarchy - -# __mUniqueIdentifier: UniqueIdentifier -# __mUniqueVariable: UniqueVariable - -# def __init__(self, reporter: bme_utils.Reporter): -# self.__mHierarchy = bme_utils.Hierarchy() -# self.__mReporter = ReporterWithHierarchy(reporter, self.__mHierarchy) - -# self.__mUniqueIdentifier = UniqueIdentifier(self.__mReporter) -# self.__mUniqueVariable = UniqueVariable(self.__mReporter) - -# _TCheckKey = typing.TypeVar('_TCheckKey') -# def __check_key(self, data: dict[str, typing.Any], key: str, expected_type: type[_TCheckKey]) -> _TCheckKey | None: -# """ -# @brief Check the existance and tyoe of value stored in given dict and key. -# @param[in] data The dict need to be checked -# @param[in] key The key for fetching value. -# @param[in] expected_type The expected type of fetched value. -# @return None if error occurs, otherwise the value stored in given dict and key. -# """ -# gotten_value = data[key] -# if gotten_value is None: -# # report no key error -# self.__mReporter.error(f'Can not find key "{key}". Did you forget it?') -# elif not isinstance(gotten_value, expected_type): -# # get the type of value -# value_type = type(gotten_value) -# # format normal error message -# err_msg: str = f'The type of value stored inside key "{key}" is incorrect. ' -# err_msg += f'Expect "{expected_type.__name__}" got "{value_type.__name__}". ' -# # add special note for easily confusing types -# # e.g. forget quote number (number literal are recognise as number accidently) -# if issubclass(expected_type, str) and issubclass(type(data), (int, float)): -# err_msg += 'Did you forgot quote the number?' -# # report type error -# self.__mReporter.error(err_msg) -# else: -# # no error, return value -# return gotten_value -# # error occurs, return null -# return None - -# def __check_self(self, data: typing.Any, expected_type: type) -> bool: -# """ -# @brief Check the type of given data. -# @return True if type matched, otherwise false. -# """ -# if data is None: -# self.__mReporter.error('Data is unexpected null.') -# elif not isinstance(data, expected_type): -# # usually this function is checking list or dict, so no scenario that user forget quote literal number. -# self.__mReporter.error(f'The type of given data is not expected. Expect "{expected_type.__name__}" got "{type(data).__name__}".') -# else: -# # no error, return okey -# return True -# # error occurs, return failed -# return False - -# # 按层次递归调用检查。 -# # 每个层次只负责当前层次的检查。 -# # 如果值为列表,字典,则在当前层次检查完其类型(容器本身,对每一项不检查),然后对每一项调用对应层次检查。 -# # 如果值不是上述类型(例如整数,浮点数,字符串等),在当前层次检查。 - -# def validate(self, assoc_file: str, prototypes: typing.Any) -> None: -# # reset hierarchy -# self.__mHierarchy.clear() -# # start to validate -# with self.__mHierarchy.safe_push(assoc_file): -# self.__validate_prototypes(prototypes) - -# def __validate_prototypes(self, prototypes: typing.Any) -> None: -# # the most outer structure must be a list -# if not self.__check_self(prototypes, list): return -# cast_prototypes = typing.cast(list[typing.Any], prototypes) -# # iterate prototype -# for prototype_index, prototype in enumerate(cast_prototypes): -# with self.__mHierarchy.safe_push(prototype_index) as layer: -# self.__validate_prototype(layer, prototype) - -# def __validate_prototype(self, layer: bme_utils.HierarchyLayer, prototype: typing.Any) -> None: -# # check whether self is a dict -# if not self.__check_self(prototype, dict): return -# cast_prototype = typing.cast(dict[str, typing.Any], prototype) - -# # clear unique field for each prototype -# self.__mUniqueVariable.clear() - -# # check identifier -# identifier = self.__check_key(cast_prototype, TOKEN_IDENTIFIER, str) -# if identifier is not None: -# # replace hierarchy -# layer.emplace(identifier) -# # check unique -# self.__mUniqueIdentifier.register(identifier) - -# # check showcase but don't use check function -# # because it is optional. -# showcase = cast_prototype[TOKEN_SHOWCASE] -# if showcase is not None: -# # we only check non-template prototype -# with self.__mHierarchy.safe_push(TOKEN_SHOWCASE): -# self.__validate_showcase(typing.cast(dict[str, typing.Any], showcase)) - -# # check params, vars, vertices, faces, instances -# # they are all list -# params = self.__check_key(cast_prototype, TOKEN_PARAMS, list) -# if params is not None: -# cast_params = typing.cast(list[typing.Any], params) -# with self.__mHierarchy.safe_push(TOKEN_PARAMS): -# for param_index, param in enumerate(cast_params): -# with self.__mHierarchy.safe_push(param_index): -# self.__validate_param(param) - -# vars = self.__check_key(cast_prototype, TOKEN_VARS, list) -# if vars is not None: -# cast_vars = typing.cast(list[typing.Any], vars) -# with self.__mHierarchy.safe_push(TOKEN_VARS): -# for var_index, var in enumerate(cast_vars): -# with self.__mHierarchy.safe_push(var_index): -# self.__validate_var(var) - -# vertices = self.__check_key(cast_prototype, TOKEN_VERTICES, list) -# if vertices is not None: -# cast_vertices = typing.cast(list[typing.Any], vertices) -# with self.__mHierarchy.safe_push(TOKEN_VERTICES): -# for vertex_index, vertex in enumerate(cast_vertices): -# with self.__mHierarchy.safe_push(vertex_index): -# self.__validate_vertex(vertex) - -# faces = self.__check_key(cast_prototype, TOKEN_FACES, list) -# if faces is not None: -# cast_faces = typing.cast(list[typing.Any], faces) -# with self.__mHierarchy.safe_push(TOKEN_FACES): -# for face_index, face in enumerate(cast_faces): -# with self.__mHierarchy.safe_push(face_index): -# self.__validate_face(face) - -# instances = self.__check_key(cast_prototype, TOKEN_INSTANCES, list) -# if instances is not None: -# cast_instances = typing.cast(list[typing.Any], instances) -# with self.__mHierarchy.safe_push(TOKEN_INSTANCES): -# for instance_index, instance in enumerate(cast_instances): -# with self.__mHierarchy.safe_push(instance_index): -# self.__validate_instance(instance) - -# def __validate_showcase(self, showcase: dict[str, typing.Any]) -> None: -# pass - -# def __validate_param(self, param: typing.Any) -> None: -# # check whether self is a dict -# if not self.__check_self(param, dict): return -# cast_param = typing.cast(dict[str, typing.Any], param) - -# # check field -# field = self.__check_key(cast_param, TOKEN_PARAMS_FIELD, str) -# if field is not None: -# self.__mUniqueVariable.register(field) - -# # check data -# self.__check_key(cast_param, TOKEN_PARAMS_DATA, str) - -# def __validate_var(self, var: typing.Any) -> None: -# # check whether self is a dict -# if not self.__check_self(var, dict): return -# cast_var = typing.cast(dict[str, typing.Any], var) - -# # check field -# field = self.__check_key(cast_var, TOKEN_VARS_FIELD, str) -# if field is not None: -# self.__mUniqueVariable.register(field) - -# # check data -# self.__check_key(cast_var, TOKEN_VARS_DATA, str) - -# def __validate_vertex(self, vertex: typing.Any) -> None: -# # check whether self is a dict -# if not self.__check_self(vertex, dict): return -# cast_vertex = typing.cast(dict[str, typing.Any], vertex) - -# # check fields -# self.__check_key(cast_vertex, TOKEN_VERTICES_SKIP, str) -# self.__check_key(cast_vertex, TOKEN_VERTICES_DATA, str) - -# def __validate_face(self, face: typing.Any) -> None: -# pass - -# def __validate_instance(self, instance: typing.Any) -> None: -# pass - - -class BMEExtractor(): - """ - A GetText extractor for BME prototype declarations. - This extractor can extract all UI infomations which will be shown on Blender first. - Then write them into caller given PO file. So that translator can translate them. - - Blender default I18N plugin can not recognise these dynamic loaded content, - so that's the reason why this class invented. - - Please note all data should be validate first, then pass to this class. - Otherwise it is undefined behavior. - """ - - __mAssocFile: str - __mHierarchy: bme_utils.Hierarchy - __mReporter: bme_utils.Reporter - __mPoWriter: simple_po.PoWriter - - def __init__(self, reporter: bme_utils.Reporter, po_writer: simple_po.PoWriter): - self.__mAssocFile = '' - self.__mHierarchy = bme_utils.Hierarchy() - self.__mReporter = reporter - self.__mPoWriter = po_writer - - def __add_translation(self, msg: str) -> None: - """ - @brief Convenient internal translation adder. - @details Add given message into PO file with auto generated hierarchy for translation context. - @param[in] msg The message for translating. - """ - self.__mPoWriter.add_entry( - msg, - CTX_TRANSLATION + '/' + self.__mHierarchy.build_hierarchy_string(), - # use associated file as extracted message to tell user where we extract it. - # put file name in hierarchy is not proper (file path may be changed when moving prototype between them). - self.__mAssocFile - ) - - def __report_duplication_error(self) -> None: - """ - @brief Convenient internal function to report duplicated translation message issue. - @details - A convenient internal used function to report issue that - the "title" field and "desc" field of the same showcase configuration entry have same content - which may cause that generated PO file is illegal. - """ - self.__mReporter.error( - 'The content of "title" and "desc" can not be the same in one entry. Please modify one of them.', - self.__mAssocFile + '/' + self.__mHierarchy.build_hierarchy_string() - ) - - def extract(self, assoc_file: str, prototypes: list[dict[str, typing.Any]]) -> None: - self.__mAssocFile = assoc_file - for prototype in prototypes: - self.__extract_prototype(prototype) - - def __extract_prototype(self, prototype: dict[str, typing.Any]) -> None: - # get identifier first - identifier: str = prototype[TOKEN_IDENTIFIER] - with self.__mHierarchy.safe_push(identifier): - # get showcase node and only write PO file if it is not template prototype - showcase: dict[str, typing.Any] | None = prototype[TOKEN_SHOWCASE] - if showcase is not None: - self.__extract_showcase(showcase) - - def __extract_showcase(self, showcase: dict[str, typing.Any]) -> None: - # export self name first - self.__add_translation(showcase[TOKEN_SHOWCASE_TITLE]) - - # iterate cfgs - cfgs: list[dict[str, typing.Any]] = showcase[TOKEN_SHOWCASE_CFGS] - for cfg_index, cfg in enumerate(cfgs): - self.__extract_showcase_cfg(cfg_index, cfg) - - def __extract_showcase_cfg(self, index: int, cfg: dict[str, typing.Any]) -> None: - # push cfg index - with self.__mHierarchy.safe_push(index): - # extract field title and description - title: str = cfg[TOKEN_SHOWCASE_CFGS_TITLE] - desc: str = cfg[TOKEN_SHOWCASE_CFGS_DESC] - - # check duplication error - # if "title" is equal to "desc" and they are not blank - if title == desc and title != "": - self.__report_duplication_error() - - # export them respectively if they are not blank - if title != "": - self.__add_translation(title) - if desc!= "": - self.__add_translation(desc) - diff --git a/scripts/bme_utils.py b/scripts/bme_utils.py deleted file mode 100644 index 60872bc..0000000 --- a/scripts/bme_utils.py +++ /dev/null @@ -1,144 +0,0 @@ -import typing -import collections -import termcolor - -class Reporter(): - """ - General reporter with context support for convenient logging. - """ - - def __init__(self): - pass - - def __report(self, type: str, msg: str, context: str | None, color: str) -> None: - # build message - strl: str = f'[{type}]' - if context is not None: - strl += f'[{context}]' - strl += ' ' + msg - # output with color - termcolor.cprint(strl, color) - - def error(self, msg: str, context: str | None = None) -> None: - """ - @brief Report an error. - @param[in] msg The message to show. - @param[in] context The context of this message, e.g. the file path. None if no context. - """ - self.__report('Error', msg, context, 'red') - - def warning(self, msg: str, context: str | None = None) -> None: - """ - @brief Report a warning. - @param[in] msg The message to show. - @param[in] context The context of this message, e.g. the file path. None if no context. - """ - self.__report('Warning', msg, context, 'yellow') - - def info(self, msg: str, context: str | None = None) -> None: - """ - @brief Report a info. - @param[in] msg The message to show. - @param[in] context The context of this message, e.g. the file path. None if no context. - """ - self.__report('Info', msg, context, 'white') - -class Hierarchy(): - """ - The hierarchy for BME validator and BME extractor. - In BME validator, it build human-readable string representing the location where error happen. - In BME extractor, it build the string used as the context of translation. - """ - - __mStack: collections.deque[str] - - def __init__(self): - self.__mStack = collections.deque() - - def push(self, item: str | int) -> None: - """ - @brief Add an item into the top of this hierarchy. - @details - If given item is string, it will be push into hierarchy directly. - If given item is integer, this function will treat it as a special case, the index. - Function will push it into hierarchy after formatting it (add a pair of bracket around it). - @param[in] item New added item. - """ - if isinstance(item, str): - self.__mStack.append(item) - elif isinstance(item, int): - self.__mStack.append(f'[{item}]') - else: - raise Exception('Unexpected type of item when pushing into hierarchy.') - - def pop(self) -> None: - """ - @brief Remove the top item from hierarchy - """ - self.__mStack.pop() - - def safe_push(self, item: str | int) -> 'HierarchyLayer': - """ - @brief The safe version of push function. - @return A with-context-supported instance which can make sure pushed item popped when leaving scope. - """ - return HierarchyLayer(self, item) - - def clear(self) -> None: - """ - @brief Clear this hierarchy. - """ - self.__mStack.clear() - - def depth(self) -> int: - """ - @brief Return the depth of this hierarchy. - @return The depth of this hierarchy. - """ - return len(self.__mStack) - - def build_hierarchy_string(self) -> str: - """ - @brief Build the string which can represent this hierarchy. - @details It just join every items with `/` as separator. - @return The built string representing this hierarchy. - """ - return '/'.join(self.__mStack) - -class HierarchyLayer(): - """ - An with-context-supported class for Hierarchy which can automatically pop item when leaving scope. - This is convenient for keeping the balance of Hierarchy (avoid programmer accidently forgetting to pop item). - """ - - __mHasPop: bool - __mAssocHierarchy: Hierarchy - - def __init__(self, assoc_hierarchy: Hierarchy, item: str | int): - self.__mAssocHierarchy = assoc_hierarchy - self.__mHasPop = False - self.__mAssocHierarchy.push(item) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - def close(self) -> None: - if not self.__mHasPop: - self.__mAssocHierarchy.pop() - self.__mHasPop = True - - def emplace(self, new_item: str | int) -> None: - """ - @brief Replace the content of top item in-place. - @details - In some cases, caller need to replace the content of top item. - For example, at the beginning, we only have index info. - After validating something, we can fetching a more human-readable info, such as name, - now we need replace the content of top item. - @param[in] new_item The new content of top item. - """ - self.__mAssocHierarchy.pop() - self.__mAssocHierarchy.push(new_item) diff --git a/scripts/simple_po.py b/scripts/simple_po.py deleted file mode 100644 index 35a1c76..0000000 --- a/scripts/simple_po.py +++ /dev/null @@ -1,99 +0,0 @@ -import typing -import io -import datetime - -class PoWriter(): - """ - The simple PO file writer. - This class is just served for writing POT files. - It may be convenient when exporting PO file for thoese whose format can not be parsed by formal tools. - """ - - __cEscapeCharsDict: typing.ClassVar[dict[str, str]] = { - '\\': '\\\\', - '"': '\\"', - '\n': '\\n', - '\t': '\\t', - } - __cEscapeCharsTable: typing.ClassVar[dict] = str.maketrans(__cEscapeCharsDict) - __mPoFile: io.TextIOWrapper - - def __init__(self, po_file_path: str, project_name: str): - # open file - self.__mPoFile = open(po_file_path, 'w', encoding = 'utf-8') - # add default header - self.__add_header(project_name) - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.close() - - def close(self) -> None: - self.__mPoFile.close() - - def __write_line(self, val: str) -> None: - self.__mPoFile.write(val) - self.__mPoFile.write('\n') - - def __escape_str(self, val: str) -> str: - """ - This function escapes a given string to make it safe to use as a C++ string literal. - @param[in] val Original string - @return Escaped string - """ - return val.translate(PoWriter.__cEscapeCharsTable) - - def __add_header(self, project_name: str) -> None: - """ - Add default header for PO file. - @param[in] project_name The project name written in file. - """ - now_datetime = datetime.datetime.now() - self.__write_line('# FIRST AUTHOR , YEAR.') - self.__write_line('msgid ""') - self.__write_line('msgstr ""') - self.__write_line(f'"Project-Id-Version: {self.__escape_str(project_name)}\\n"') - self.__write_line(f'"POT-Creation-Date: {now_datetime.strftime("%Y-%m-%d %H:%M%Z")}\\n"') - self.__write_line('"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"') - self.__write_line('"Last-Translator: FULL NAME \\n"') - self.__write_line('"Language-Team: LANGUAGE \\n"') - self.__write_line('"Language: __POT__\\n"') - self.__write_line('"MIME-Version: 1.0\\n"') - self.__write_line('"Content-Type: text/plain; charset=UTF-8\\n"') - self.__write_line('"Content-Transfer-Encoding: 8bit\\n"') - self.__write_line('"X-Generator: simple_po.PoWriter\\n"') - - def add_entry(self, msg: str, msg_context: str | None = None, extracted_comment: str | None = None, reference: str | None = None) -> None: - """ - @brief Write an entry into PO file with given arguments. - @details - Please note this function will NOT check whether there already is a duplicated entry which has been written. - You must check this on your own. - @param[in] msg The message string need to be translated. - @param[in] msg_context The context of this message. - @param[in] extracted_comment The extracted comment of this message. None if no reference. Line breaker is not allowed. - @param[in] reference The code refernece of this message. None if no reference. Line breaker is not allowed. - """ - # empty string will not be translated - if msg == '': return - - # write blank line first - self.__write_line('') - if extracted_comment: - self.__write_line(f'#. {extracted_comment}') - if reference: - self.__write_line(f'#: {reference}') - if msg_context: - self.__write_line(f'msgctxt "{self.__escape_str(msg_context)}"') - self.__write_line(f'msgid "{self.__escape_str(msg)}"') - self.__write_line('msgstr ""') - - def build_code_reference(self, code_file_path: str, code_line_number: int) -> str: - """ - A convenient function to build code reference string used when adding entry. - @param[in] code_file_path The path to associated code file. - @param[in] code_line_number The line number of associated code within given file. - """ - return f'{code_file_path}:{code_line_number}'