Initial implementation of XAP protocol.
This commit is contained in:
		
							parent
							
								
									f4c447f2df
								
							
						
					
					
						commit
						eba91c6e28
					
				
					 34 changed files with 1934 additions and 4 deletions
				
			
		
							
								
								
									
										65
									
								
								lib/python/qmk/casing.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										65
									
								
								lib/python/qmk/casing.py
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,65 @@
 | 
			
		|||
"""This script handles conversion between snake and camel casing.
 | 
			
		||||
"""
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
_words_expr = re.compile(r"([a-zA-Z][^A-Z0-9]*|[0-9]+)")
 | 
			
		||||
_lower_snake_case_expr = re.compile(r'^[a-z][a-z0-9_]*$')
 | 
			
		||||
_upper_snake_case_expr = re.compile(r'^[A-Z][A-Z0-9_]*$')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _is_snake_case(str):
 | 
			
		||||
    """Checks if the supplied string is already in snake case.
 | 
			
		||||
    """
 | 
			
		||||
    match = _lower_snake_case_expr.match(str)
 | 
			
		||||
    if match:
 | 
			
		||||
        return True
 | 
			
		||||
    match = _upper_snake_case_expr.match(str)
 | 
			
		||||
    if match:
 | 
			
		||||
        return True
 | 
			
		||||
    return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _split_snake_case(str):
 | 
			
		||||
    """Splits up a string based on underscores, if it's in snake casing.
 | 
			
		||||
    """
 | 
			
		||||
    if _is_snake_case(str):
 | 
			
		||||
        return [s.lower() for s in str.split("_")]
 | 
			
		||||
    return str
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _split_camel_case(str):
 | 
			
		||||
    """Splits up a string based on capitalised camel casing.
 | 
			
		||||
    """
 | 
			
		||||
    return _words_expr.findall(str)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _split_cased_words(str):
 | 
			
		||||
    return _split_snake_case(str) if _is_snake_case(str) else _split_camel_case(str)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_snake(str):
 | 
			
		||||
    str = "_".join([word.strip().lower() for word in _split_cased_words(str)])
 | 
			
		||||
 | 
			
		||||
    # Fix acronyms
 | 
			
		||||
    str = str.replace('i_d', 'id')
 | 
			
		||||
    str = str.replace('x_a_p', 'xap')
 | 
			
		||||
    str = str.replace('q_m_k', 'qmk')
 | 
			
		||||
 | 
			
		||||
    return str
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_upper_snake(str):
 | 
			
		||||
    return to_snake(str).upper()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def to_camel(str):
 | 
			
		||||
    def _acronym(w):
 | 
			
		||||
        if w.strip().lower() == 'qmk':
 | 
			
		||||
            return 'QMK'
 | 
			
		||||
        elif w.strip().lower() == 'xap':
 | 
			
		||||
            return 'XAP'
 | 
			
		||||
        elif w.strip().lower() == 'id':
 | 
			
		||||
            return 'ID'
 | 
			
		||||
        return w.title()
 | 
			
		||||
 | 
			
		||||
    return "".join([_acronym(word) for word in _split_cased_words(str)])
 | 
			
		||||
| 
						 | 
				
			
			@ -66,6 +66,9 @@ subcommands = [
 | 
			
		|||
    'qmk.cli.new.keymap',
 | 
			
		||||
    'qmk.cli.pyformat',
 | 
			
		||||
    'qmk.cli.pytest',
 | 
			
		||||
    'qmk.cli.xap.generate_docs',
 | 
			
		||||
    'qmk.cli.xap.generate_json',
 | 
			
		||||
    'qmk.cli.xap.generate_qmk',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										0
									
								
								lib/python/qmk/cli/xap/__init__.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										0
									
								
								lib/python/qmk/cli/xap/__init__.py
									
										
									
									
									
										Executable file
									
								
							
							
								
								
									
										11
									
								
								lib/python/qmk/cli/xap/generate_docs.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										11
									
								
								lib/python/qmk/cli/xap/generate_docs.py
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
"""This script generates the XAP protocol documentation.
 | 
			
		||||
"""
 | 
			
		||||
from milc import cli
 | 
			
		||||
from qmk.xap.gen_docs.generator import generate_docs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.subcommand('Generates the XAP protocol documentation.', hidden=False if cli.config.user.developer else True)
 | 
			
		||||
def xap_generate_docs(cli):
 | 
			
		||||
    """Generates the XAP protocol documentation by merging the definitions files, and producing the corresponding Markdown document under `/docs/`.
 | 
			
		||||
    """
 | 
			
		||||
    generate_docs()
 | 
			
		||||
							
								
								
									
										13
									
								
								lib/python/qmk/cli/xap/generate_json.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										13
									
								
								lib/python/qmk/cli/xap/generate_json.py
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
"""This script generates the consolidated XAP protocol definitions.
 | 
			
		||||
"""
 | 
			
		||||
import hjson
 | 
			
		||||
from milc import cli
 | 
			
		||||
from qmk.xap.common import latest_xap_defs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.subcommand('Generates the consolidated XAP protocol definitions.', hidden=False if cli.config.user.developer else True)
 | 
			
		||||
def xap_generate_json(cli):
 | 
			
		||||
    """Generates the consolidated XAP protocol definitions.
 | 
			
		||||
    """
 | 
			
		||||
    defs = latest_xap_defs()
 | 
			
		||||
    print(hjson.dumps(defs))
 | 
			
		||||
							
								
								
									
										24
									
								
								lib/python/qmk/cli/xap/generate_qmk.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										24
									
								
								lib/python/qmk/cli/xap/generate_qmk.py
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
"""This script generates the XAP protocol generated sources to be compiled into QMK firmware.
 | 
			
		||||
"""
 | 
			
		||||
from milc import cli
 | 
			
		||||
 | 
			
		||||
from qmk.path import normpath
 | 
			
		||||
from qmk.xap.gen_firmware.inline_generator import generate_inline
 | 
			
		||||
from qmk.xap.gen_firmware.header_generator import generate_header
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.argument('-o', '--output', type=normpath, help='File to write to')
 | 
			
		||||
@cli.subcommand('Generates the XAP protocol include.', hidden=False if cli.config.user.developer else True)
 | 
			
		||||
def xap_generate_qmk_inc(cli):
 | 
			
		||||
    """Generates the XAP protocol inline codegen file, generated during normal build.
 | 
			
		||||
    """
 | 
			
		||||
    generate_inline(cli.args.output)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cli.argument('-o', '--output', type=normpath, help='File to write to')
 | 
			
		||||
@cli.argument('-kb', '--keyboard', help='Name of the keyboard')
 | 
			
		||||
@cli.subcommand('Generates the XAP protocol include.', hidden=False if cli.config.user.developer else True)
 | 
			
		||||
def xap_generate_qmk_h(cli):
 | 
			
		||||
    """Generates the XAP protocol header file, generated during normal build.
 | 
			
		||||
    """
 | 
			
		||||
    generate_header(cli.args.output, cli.args.keyboard)
 | 
			
		||||
| 
						 | 
				
			
			@ -87,11 +87,14 @@ def create_make_command(keyboard, keymap, target=None, parallel=1, **env_vars):
 | 
			
		|||
    return create_make_target(':'.join(make_args), parallel, **env_vars)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_git_version(current_time, repo_dir='.', check_dir='.'):
 | 
			
		||||
def get_git_version(current_time=None, repo_dir='.', check_dir='.'):
 | 
			
		||||
    """Returns the current git version for a repo, or the current time.
 | 
			
		||||
    """
 | 
			
		||||
    git_describe_cmd = ['git', 'describe', '--abbrev=6', '--dirty', '--always', '--tags']
 | 
			
		||||
 | 
			
		||||
    if current_time is None:
 | 
			
		||||
        current_time = strftime(time_fmt)
 | 
			
		||||
 | 
			
		||||
    if repo_dir != '.':
 | 
			
		||||
        repo_dir = Path('lib') / repo_dir
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -118,7 +121,7 @@ def create_version_h(skip_git=False, skip_all=False):
 | 
			
		|||
    if skip_all:
 | 
			
		||||
        current_time = "1970-01-01-00:00:00"
 | 
			
		||||
    else:
 | 
			
		||||
        current_time = strftime(time_fmt)
 | 
			
		||||
        current_time = None
 | 
			
		||||
 | 
			
		||||
    if skip_git:
 | 
			
		||||
        git_version = "NA"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
"""Information that should be available to the python library.
 | 
			
		||||
"""
 | 
			
		||||
from os import environ
 | 
			
		||||
from datetime import date
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
# The root of the qmk_firmware tree.
 | 
			
		||||
| 
						 | 
				
			
			@ -36,3 +37,92 @@ LED_INDICATORS = {
 | 
			
		|||
# Constants that should match their counterparts in make
 | 
			
		||||
BUILD_DIR = environ.get('BUILD_DIR', '.build')
 | 
			
		||||
KEYBOARD_OUTPUT_PREFIX = f'{BUILD_DIR}/obj_'
 | 
			
		||||
 | 
			
		||||
# Headers for generated files
 | 
			
		||||
this_year = date.today().year
 | 
			
		||||
GPL2_HEADER_C_LIKE = f'''\
 | 
			
		||||
/* Copyright {this_year} QMK
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 2 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
GPL2_HEADER_SH_LIKE = f'''\
 | 
			
		||||
# Copyright {this_year} QMK
 | 
			
		||||
#
 | 
			
		||||
# This program is free software: you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU General Public License as published by
 | 
			
		||||
# the Free Software Foundation, either version 2 of the License, or
 | 
			
		||||
# (at your option) any later version.
 | 
			
		||||
#
 | 
			
		||||
# This program is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
# GNU General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License
 | 
			
		||||
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
GENERATED_HEADER_C_LIKE = '''\
 | 
			
		||||
/*******************************************************************************
 | 
			
		||||
  88888888888 888      d8b                .d888 d8b 888               d8b
 | 
			
		||||
      888     888      Y8P               d88P"  Y8P 888               Y8P
 | 
			
		||||
      888     888                        888        888
 | 
			
		||||
      888     88888b.  888 .d8888b       888888 888 888  .d88b.       888 .d8888b
 | 
			
		||||
      888     888 "88b 888 88K           888    888 888 d8P  Y8b      888 88K
 | 
			
		||||
      888     888  888 888 "Y8888b.      888    888 888 88888888      888 "Y8888b.
 | 
			
		||||
      888     888  888 888      X88      888    888 888 Y8b.          888      X88
 | 
			
		||||
      888     888  888 888  88888P'      888    888 888  "Y8888       888  88888P'
 | 
			
		||||
 | 
			
		||||
                                                        888                 888
 | 
			
		||||
                                                        888                 888
 | 
			
		||||
                                                        888                 888
 | 
			
		||||
     .d88b.   .d88b.  88888b.   .d88b.  888d888 8888b.  888888 .d88b.   .d88888
 | 
			
		||||
    d88P"88b d8P  Y8b 888 "88b d8P  Y8b 888P"      "88b 888   d8P  Y8b d88" 888
 | 
			
		||||
    888  888 88888888 888  888 88888888 888    .d888888 888   88888888 888  888
 | 
			
		||||
    Y88b 888 Y8b.     888  888 Y8b.     888    888  888 Y88b. Y8b.     Y88b 888
 | 
			
		||||
     "Y88888  "Y8888  888  888  "Y8888  888    "Y888888  "Y888 "Y8888   "Y88888
 | 
			
		||||
         888
 | 
			
		||||
    Y8b d88P
 | 
			
		||||
     "Y88P"
 | 
			
		||||
*******************************************************************************/
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
GENERATED_HEADER_SH_LIKE = '''\
 | 
			
		||||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
# 88888888888 888      d8b                .d888 d8b 888               d8b
 | 
			
		||||
#     888     888      Y8P               d88P"  Y8P 888               Y8P
 | 
			
		||||
#     888     888                        888        888
 | 
			
		||||
#     888     88888b.  888 .d8888b       888888 888 888  .d88b.       888 .d8888b
 | 
			
		||||
#     888     888 "88b 888 88K           888    888 888 d8P  Y8b      888 88K
 | 
			
		||||
#     888     888  888 888 "Y8888b.      888    888 888 88888888      888 "Y8888b.
 | 
			
		||||
#     888     888  888 888      X88      888    888 888 Y8b.          888      X88
 | 
			
		||||
#     888     888  888 888  88888P'      888    888 888  "Y8888       888  88888P'
 | 
			
		||||
#
 | 
			
		||||
#                                                       888                 888
 | 
			
		||||
#                                                       888                 888
 | 
			
		||||
#                                                       888                 888
 | 
			
		||||
#    .d88b.   .d88b.  88888b.   .d88b.  888d888 8888b.  888888 .d88b.   .d88888
 | 
			
		||||
#   d88P"88b d8P  Y8b 888 "88b d8P  Y8b 888P"      "88b 888   d8P  Y8b d88" 888
 | 
			
		||||
#   888  888 88888888 888  888 88888888 888    .d888888 888   88888888 888  888
 | 
			
		||||
#   Y88b 888 Y8b.     888  888 Y8b.     888    888  888 Y88b. Y8b.     Y88b 888
 | 
			
		||||
#    "Y88888  "Y8888  888  888  "Y8888  888    "Y888888  "Y888 "Y8888   "Y88888
 | 
			
		||||
#        888
 | 
			
		||||
#   Y8b d88P
 | 
			
		||||
#    "Y88P"
 | 
			
		||||
#
 | 
			
		||||
################################################################################
 | 
			
		||||
'''
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										0
									
								
								lib/python/qmk/xap/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								lib/python/qmk/xap/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										81
									
								
								lib/python/qmk/xap/common.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										81
									
								
								lib/python/qmk/xap/common.py
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,81 @@
 | 
			
		|||
"""This script handles the XAP protocol data files.
 | 
			
		||||
"""
 | 
			
		||||
import re
 | 
			
		||||
import hjson
 | 
			
		||||
from typing import OrderedDict
 | 
			
		||||
 | 
			
		||||
from qmk.constants import QMK_FIRMWARE
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _merge_ordered_dicts(dicts):
 | 
			
		||||
    """Merges nested OrderedDict objects resulting from reading a hjson file.
 | 
			
		||||
 | 
			
		||||
    Later input dicts overrides earlier dicts for plain values.
 | 
			
		||||
    Arrays will be appended. If the first entry of an array is "!reset!", the contents of the array will be cleared and replaced with RHS.
 | 
			
		||||
    Dictionaries will be recursively merged. If any entry is "!reset!", the contents of the dictionary will be cleared and replaced with RHS.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    result = OrderedDict()
 | 
			
		||||
 | 
			
		||||
    def add_entry(target, k, v):
 | 
			
		||||
        if k in target and isinstance(v, OrderedDict):
 | 
			
		||||
            if "!reset!" in v:
 | 
			
		||||
                target[k] = v
 | 
			
		||||
            else:
 | 
			
		||||
                target[k] = _merge_ordered_dicts([target[k], v])
 | 
			
		||||
            if "!reset!" in target[k]:
 | 
			
		||||
                del target[k]["!reset!"]
 | 
			
		||||
        elif k in target and isinstance(v, list):
 | 
			
		||||
            if v[0] == '!reset!':
 | 
			
		||||
                target[k] = v[1:]
 | 
			
		||||
            else:
 | 
			
		||||
                target[k] = target[k] + v
 | 
			
		||||
        else:
 | 
			
		||||
            target[k] = v
 | 
			
		||||
 | 
			
		||||
    for d in dicts:
 | 
			
		||||
        for (k, v) in d.items():
 | 
			
		||||
            add_entry(result, k, v)
 | 
			
		||||
 | 
			
		||||
    return result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_xap_definition_files():
 | 
			
		||||
    """Get the sorted list of XAP definition files, from <QMK>/data/xap.
 | 
			
		||||
    """
 | 
			
		||||
    xap_defs = QMK_FIRMWARE / "data" / "xap"
 | 
			
		||||
    return list(sorted(xap_defs.glob('**/xap_*.hjson')))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_xap_definitions(original, new):
 | 
			
		||||
    """Creates a new XAP definition object based on an original and the new supplied object.
 | 
			
		||||
 | 
			
		||||
    Both inputs must be of type OrderedDict.
 | 
			
		||||
    Later input dicts overrides earlier dicts for plain values.
 | 
			
		||||
    Arrays will be appended. If the first entry of an array is "!reset!", the contents of the array will be cleared and replaced with RHS.
 | 
			
		||||
    Dictionaries will be recursively merged. If any entry is "!reset!", the contents of the dictionary will be cleared and replaced with RHS.
 | 
			
		||||
    """
 | 
			
		||||
    if original is None:
 | 
			
		||||
        original = OrderedDict()
 | 
			
		||||
    return _merge_ordered_dicts([original, new])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def latest_xap_defs():
 | 
			
		||||
    """Gets the latest version of the XAP definitions.
 | 
			
		||||
    """
 | 
			
		||||
    definitions = [hjson.load(file.open(encoding='utf-8')) for file in get_xap_definition_files()]
 | 
			
		||||
    return _merge_ordered_dicts(definitions)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def route_conditions(route_stack):
 | 
			
		||||
    """Handles building the C preprocessor conditional based on the current route.
 | 
			
		||||
    """
 | 
			
		||||
    conditions = []
 | 
			
		||||
    for route in route_stack:
 | 
			
		||||
        if 'enable_if_preprocessor' in route:
 | 
			
		||||
            conditions.append(route['enable_if_preprocessor'])
 | 
			
		||||
 | 
			
		||||
    if len(conditions) == 0:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    return "(" + ' && '.join([f'({c})' for c in conditions]) + ")"
 | 
			
		||||
							
								
								
									
										0
									
								
								lib/python/qmk/xap/gen_client_js/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								lib/python/qmk/xap/gen_client_js/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								lib/python/qmk/xap/gen_docs/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								lib/python/qmk/xap/gen_docs/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										103
									
								
								lib/python/qmk/xap/gen_docs/generator.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										103
									
								
								lib/python/qmk/xap/gen_docs/generator.py
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,103 @@
 | 
			
		|||
"""This script generates the XAP protocol documentation.
 | 
			
		||||
"""
 | 
			
		||||
import hjson
 | 
			
		||||
from qmk.constants import QMK_FIRMWARE
 | 
			
		||||
from qmk.xap.common import get_xap_definition_files, update_xap_definitions
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _update_type_docs(overall):
 | 
			
		||||
    defs = overall['type_docs']
 | 
			
		||||
 | 
			
		||||
    type_docs = []
 | 
			
		||||
    for (k, v) in sorted(defs.items(), key=lambda x: x[0]):
 | 
			
		||||
        type_docs.append(f'| _{k}_ | {v} |')
 | 
			
		||||
 | 
			
		||||
    desc_str = "\n".join(type_docs)
 | 
			
		||||
 | 
			
		||||
    overall['documentation']['!type_docs!'] = f'''\
 | 
			
		||||
| Name | Definition |
 | 
			
		||||
| -- | -- |
 | 
			
		||||
{desc_str}
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _update_term_definitions(overall):
 | 
			
		||||
    defs = overall['term_definitions']
 | 
			
		||||
 | 
			
		||||
    term_descriptions = []
 | 
			
		||||
    for (k, v) in sorted(defs.items(), key=lambda x: x[0]):
 | 
			
		||||
        term_descriptions.append(f'| _{k}_ | {v} |')
 | 
			
		||||
 | 
			
		||||
    desc_str = "\n".join(term_descriptions)
 | 
			
		||||
 | 
			
		||||
    overall['documentation']['!term_definitions!'] = f'''\
 | 
			
		||||
| Name | Definition |
 | 
			
		||||
| -- | -- |
 | 
			
		||||
{desc_str}
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _update_response_flags(overall):
 | 
			
		||||
    flags = overall['response_flags']['bits']
 | 
			
		||||
    for n in range(0, 8):
 | 
			
		||||
        if str(n) not in flags:
 | 
			
		||||
            flags[str(n)] = {"name": "-", "description": "-"}
 | 
			
		||||
 | 
			
		||||
    header = '| ' + " | ".join([f'Bit {n}' for n in range(7, -1, -1)]) + ' |'
 | 
			
		||||
    dividers = '|' + "|".join(['--' for n in range(7, -1, -1)]) + '|'
 | 
			
		||||
    bit_names = '| ' + " | ".join([flags[str(n)]['name'] for n in range(7, -1, -1)]) + ' |'
 | 
			
		||||
 | 
			
		||||
    bit_descriptions = ''
 | 
			
		||||
    for n in range(7, -1, -1):
 | 
			
		||||
        bit_desc = flags[str(n)]
 | 
			
		||||
        if bit_desc['name'] != '-':
 | 
			
		||||
            desc = bit_desc['description']
 | 
			
		||||
            bit_descriptions = bit_descriptions + f'\n* `Bit {n}`: {desc}'
 | 
			
		||||
 | 
			
		||||
    overall['documentation']['!response_flags!'] = f'''\
 | 
			
		||||
{header}
 | 
			
		||||
{dividers}
 | 
			
		||||
{bit_names}
 | 
			
		||||
{bit_descriptions}
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_docs():
 | 
			
		||||
    """Generates the XAP protocol documentation by merging the definitions files, and producing the corresponding Markdown document under `/docs/`.
 | 
			
		||||
    """
 | 
			
		||||
    docs_list = []
 | 
			
		||||
 | 
			
		||||
    overall = None
 | 
			
		||||
    for file in get_xap_definition_files():
 | 
			
		||||
 | 
			
		||||
        overall = update_xap_definitions(overall, hjson.load(file.open(encoding='utf-8')))
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            if 'type_docs' in overall:
 | 
			
		||||
                _update_type_docs(overall)
 | 
			
		||||
            if 'term_definitions' in overall:
 | 
			
		||||
                _update_term_definitions(overall)
 | 
			
		||||
            if 'response_flags' in overall:
 | 
			
		||||
                _update_response_flags(overall)
 | 
			
		||||
        except:
 | 
			
		||||
            print(hjson.dumps(overall))
 | 
			
		||||
            exit(1)
 | 
			
		||||
 | 
			
		||||
        output_doc = QMK_FIRMWARE / "docs" / f"{file.stem}.md"
 | 
			
		||||
        docs_list.append(output_doc)
 | 
			
		||||
 | 
			
		||||
        with open(output_doc, "w", encoding='utf-8') as out_file:
 | 
			
		||||
            for e in overall['documentation']['order']:
 | 
			
		||||
                out_file.write(overall['documentation'][e].strip())
 | 
			
		||||
                out_file.write('\n\n')
 | 
			
		||||
 | 
			
		||||
    output_doc = QMK_FIRMWARE / "docs" / f"xap_protocol.md"
 | 
			
		||||
    with open(output_doc, "w", encoding='utf-8') as out_file:
 | 
			
		||||
        out_file.write('''\
 | 
			
		||||
# XAP Protocol Reference
 | 
			
		||||
 | 
			
		||||
''')
 | 
			
		||||
 | 
			
		||||
        for file in reversed(sorted(docs_list)):
 | 
			
		||||
            ver = file.stem[4:]
 | 
			
		||||
            out_file.write(f'* [XAP Version {ver}]({file.name})\n')
 | 
			
		||||
							
								
								
									
										0
									
								
								lib/python/qmk/xap/gen_firmware/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								lib/python/qmk/xap/gen_firmware/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										136
									
								
								lib/python/qmk/xap/gen_firmware/header_generator.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										136
									
								
								lib/python/qmk/xap/gen_firmware/header_generator.py
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,136 @@
 | 
			
		|||
"""This script generates the XAP protocol generated header to be compiled into QMK.
 | 
			
		||||
"""
 | 
			
		||||
import re
 | 
			
		||||
import pyhash
 | 
			
		||||
 | 
			
		||||
from qmk.commands import get_git_version
 | 
			
		||||
from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
 | 
			
		||||
from qmk.xap.common import latest_xap_defs, route_conditions
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _append_route_defines(lines, container, container_id=None, route_stack=None):
 | 
			
		||||
    """Handles building the list of the XAP routes, combining parent and child names together, as well as the route number.
 | 
			
		||||
    """
 | 
			
		||||
    if route_stack is None:
 | 
			
		||||
        route_stack = [container]
 | 
			
		||||
    else:
 | 
			
		||||
        route_stack.append(container)
 | 
			
		||||
 | 
			
		||||
    route_name = '_'.join([r['define'] for r in route_stack])
 | 
			
		||||
 | 
			
		||||
    if container_id:
 | 
			
		||||
        lines.append(f'#define {route_name} {container_id}')
 | 
			
		||||
 | 
			
		||||
    if 'routes' in container:
 | 
			
		||||
        for route_id in container['routes']:
 | 
			
		||||
            route = container['routes'][route_id]
 | 
			
		||||
            _append_route_defines(lines, route, route_id, route_stack)
 | 
			
		||||
 | 
			
		||||
    route_stack.pop()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _append_route_masks(lines, container, container_id=None, route_stack=None):
 | 
			
		||||
    """Handles creating the equivalent XAP route masks, for capabilities checks. Forces value of `0` if disabled in the firmware.
 | 
			
		||||
    """
 | 
			
		||||
    if route_stack is None:
 | 
			
		||||
        route_stack = [container]
 | 
			
		||||
    else:
 | 
			
		||||
        route_stack.append(container)
 | 
			
		||||
 | 
			
		||||
    route_name = '_'.join([r['define'] for r in route_stack])
 | 
			
		||||
    condition = route_conditions(route_stack)
 | 
			
		||||
 | 
			
		||||
    if container_id:
 | 
			
		||||
        if condition:
 | 
			
		||||
            lines.append('')
 | 
			
		||||
            lines.append(f'#if {condition}')
 | 
			
		||||
 | 
			
		||||
        lines.append(f'#define {route_name}_MASK (1ul << ({route_name}))')
 | 
			
		||||
 | 
			
		||||
        if condition:
 | 
			
		||||
            lines.append(f'#else  // {condition}')
 | 
			
		||||
            lines.append(f'#define {route_name}_MASK 0')
 | 
			
		||||
            lines.append(f'#endif  // {condition}')
 | 
			
		||||
            lines.append('')
 | 
			
		||||
 | 
			
		||||
    if 'routes' in container:
 | 
			
		||||
        for route_id in container['routes']:
 | 
			
		||||
            route = container['routes'][route_id]
 | 
			
		||||
            _append_route_masks(lines, route, route_id, route_stack)
 | 
			
		||||
 | 
			
		||||
    route_stack.pop()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _append_route_capabilities(lines, container, container_id=None, route_stack=None):
 | 
			
		||||
    """Handles creating the equivalent XAP route masks, for capabilities checks. Forces value of `0` if disabled in the firmware.
 | 
			
		||||
    """
 | 
			
		||||
    if route_stack is None:
 | 
			
		||||
        route_stack = [container]
 | 
			
		||||
    else:
 | 
			
		||||
        route_stack.append(container)
 | 
			
		||||
 | 
			
		||||
    route_name = '_'.join([r['define'] for r in route_stack])
 | 
			
		||||
 | 
			
		||||
    if 'routes' in container:
 | 
			
		||||
        lines.append('')
 | 
			
		||||
        lines.append(f'#define {route_name}_CAPABILITIES (0 \\')
 | 
			
		||||
 | 
			
		||||
        if 'routes' in container:
 | 
			
		||||
            for route_id in container['routes']:
 | 
			
		||||
                route = container['routes'][route_id]
 | 
			
		||||
                route_stack.append(route)
 | 
			
		||||
                child_name = '_'.join([r['define'] for r in route_stack])
 | 
			
		||||
                lines.append(f'  | ({child_name}_MASK) \\')
 | 
			
		||||
                route_stack.pop()
 | 
			
		||||
 | 
			
		||||
        lines.append('  )')
 | 
			
		||||
 | 
			
		||||
    if 'routes' in container:
 | 
			
		||||
        for route_id in container['routes']:
 | 
			
		||||
            route = container['routes'][route_id]
 | 
			
		||||
            _append_route_capabilities(lines, route, route_id, route_stack)
 | 
			
		||||
 | 
			
		||||
    route_stack.pop()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_header(output_file, keyboard):
 | 
			
		||||
    """Generates the XAP protocol header file, generated during normal build.
 | 
			
		||||
    """
 | 
			
		||||
    xap_defs = latest_xap_defs()
 | 
			
		||||
 | 
			
		||||
    # Preamble
 | 
			
		||||
    lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#pragma once', '']
 | 
			
		||||
 | 
			
		||||
    # Versions
 | 
			
		||||
    prog = re.compile(r'^(\d+)\.(\d+)\.(\d+)')
 | 
			
		||||
    b = prog.match(xap_defs['version'])
 | 
			
		||||
    lines.append(f'#define XAP_BCD_VERSION 0x{int(b.group(1)):02d}{int(b.group(2)):02d}{int(b.group(3)):04d}ul')
 | 
			
		||||
    b = prog.match(get_git_version())
 | 
			
		||||
    lines.append(f'#define QMK_BCD_VERSION 0x{int(b.group(1)):02d}{int(b.group(2)):02d}{int(b.group(3)):04d}ul')
 | 
			
		||||
    keyboard_id = pyhash.murmur3_32()(keyboard)
 | 
			
		||||
    lines.append(f'#define XAP_KEYBOARD_IDENTIFIER 0x{keyboard_id:08X}ul')
 | 
			
		||||
    lines.append('')
 | 
			
		||||
 | 
			
		||||
    # Append the route and command defines
 | 
			
		||||
    _append_route_defines(lines, xap_defs)
 | 
			
		||||
    lines.append('')
 | 
			
		||||
    _append_route_masks(lines, xap_defs)
 | 
			
		||||
    lines.append('')
 | 
			
		||||
    _append_route_capabilities(lines, xap_defs)
 | 
			
		||||
    lines.append('')
 | 
			
		||||
 | 
			
		||||
    # Generate the full output
 | 
			
		||||
    xap_generated_inl = '\n'.join(lines)
 | 
			
		||||
 | 
			
		||||
    # Clean up newlines
 | 
			
		||||
    while "\n\n\n" in xap_generated_inl:
 | 
			
		||||
        xap_generated_inl = xap_generated_inl.replace("\n\n\n", "\n\n")
 | 
			
		||||
 | 
			
		||||
    if output_file:
 | 
			
		||||
        if output_file.name == '-':
 | 
			
		||||
            print(xap_generated_inl)
 | 
			
		||||
        else:
 | 
			
		||||
            output_file.parent.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
            if output_file.exists():
 | 
			
		||||
                output_file.replace(output_file.parent / (output_file.name + '.bak'))
 | 
			
		||||
            output_file.write_text(xap_generated_inl)
 | 
			
		||||
							
								
								
									
										222
									
								
								lib/python/qmk/xap/gen_firmware/inline_generator.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										222
									
								
								lib/python/qmk/xap/gen_firmware/inline_generator.py
									
										
									
									
									
										Executable file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,222 @@
 | 
			
		|||
"""This script generates the XAP protocol generated header to be compiled into QMK.
 | 
			
		||||
"""
 | 
			
		||||
import pyhash
 | 
			
		||||
 | 
			
		||||
from qmk.casing import to_snake
 | 
			
		||||
from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE
 | 
			
		||||
from qmk.xap.common import latest_xap_defs, route_conditions
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_c_type(xap_type):
 | 
			
		||||
    if xap_type == 'bool':
 | 
			
		||||
        return 'bool'
 | 
			
		||||
    elif xap_type == 'u8':
 | 
			
		||||
        return 'uint8_t'
 | 
			
		||||
    elif xap_type == 'u16':
 | 
			
		||||
        return 'uint16_t'
 | 
			
		||||
    elif xap_type == 'u32':
 | 
			
		||||
        return 'uint32_t'
 | 
			
		||||
    elif xap_type == 'u64':
 | 
			
		||||
        return 'uint64_t'
 | 
			
		||||
    elif xap_type == 'struct':
 | 
			
		||||
        return 'struct'
 | 
			
		||||
    elif xap_type == 'string':
 | 
			
		||||
        return 'const char *'
 | 
			
		||||
    return 'unknown'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_route_type(container):
 | 
			
		||||
    if 'routes' in container:
 | 
			
		||||
        return 'XAP_ROUTE'
 | 
			
		||||
    elif 'return_constant' in container:
 | 
			
		||||
        if container['return_type'] == 'u32':
 | 
			
		||||
            return 'XAP_VALUE'
 | 
			
		||||
        elif container['return_type'] == 'struct':
 | 
			
		||||
            return 'XAP_CONST_MEM'
 | 
			
		||||
        elif container['return_type'] == 'string':
 | 
			
		||||
            return 'XAP_CONST_MEM'
 | 
			
		||||
    elif 'return_getter' in container:
 | 
			
		||||
        if container['return_type'] == 'u32':
 | 
			
		||||
            return 'XAP_GETTER'
 | 
			
		||||
    return 'UNSUPPORTED'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _append_routing_table_declaration(lines, container, container_id, route_stack):
 | 
			
		||||
    route_stack.append(container)
 | 
			
		||||
 | 
			
		||||
    route_name = to_snake('_'.join([r['define'] for r in route_stack]))
 | 
			
		||||
 | 
			
		||||
    if 'routes' in container:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    elif 'return_constant' in container:
 | 
			
		||||
 | 
			
		||||
        if container['return_type'] == 'u32':
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
        elif container['return_type'] == 'struct':
 | 
			
		||||
            lines.append('')
 | 
			
		||||
            lines.append(f'static const struct {route_name}_t {{')
 | 
			
		||||
 | 
			
		||||
            for member in container['return_struct_members']:
 | 
			
		||||
                member_type = _get_c_type(member['type'])
 | 
			
		||||
                member_name = to_snake(member['name'])
 | 
			
		||||
                lines.append(f'    const {member_type} {member_name};')
 | 
			
		||||
 | 
			
		||||
            lines.append(f'}} {route_name}_data PROGMEM = {{')
 | 
			
		||||
 | 
			
		||||
            for constant in container['return_constant']:
 | 
			
		||||
                lines.append(f'    {constant},')
 | 
			
		||||
 | 
			
		||||
            lines.append(f'}};')
 | 
			
		||||
 | 
			
		||||
        elif container['return_type'] == 'string':
 | 
			
		||||
            constant = container['return_constant']
 | 
			
		||||
            lines.append('')
 | 
			
		||||
            lines.append(f'static const char {route_name}_str[] PROGMEM = {constant};')
 | 
			
		||||
 | 
			
		||||
    elif 'return_getter' in container:
 | 
			
		||||
 | 
			
		||||
        if container['return_type'] == 'u32':
 | 
			
		||||
            lines.append('')
 | 
			
		||||
            lines.append(f'extern uint32_t {route_name}_getter(void);')
 | 
			
		||||
 | 
			
		||||
        elif container['return_type'] == 'struct':
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
    route_stack.pop()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _append_routing_table_entry_flags(lines, container, container_id, route_stack):
 | 
			
		||||
    is_secure = 1 if ('secure' in container and container['secure'] is True) else 0
 | 
			
		||||
    lines.append(f'        .flags = {{')
 | 
			
		||||
    lines.append(f'            .type = {_get_route_type(container)},')
 | 
			
		||||
    lines.append(f'            .is_secure = {is_secure},')
 | 
			
		||||
    lines.append(f'        }},')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _append_routing_table_entry_route(lines, container, container_id, route_stack):
 | 
			
		||||
    route_name = to_snake('_'.join([r['define'] for r in route_stack]))
 | 
			
		||||
    lines.append(f'        .child_routes = {route_name}_table,')
 | 
			
		||||
    lines.append(f'        .child_routes_len = sizeof({route_name}_table)/sizeof(xap_route_t),')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _append_routing_table_entry_u32value(lines, container, container_id, route_stack):
 | 
			
		||||
    value = container['return_constant']
 | 
			
		||||
    lines.append(f'        .u32value = {value},')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _append_routing_table_entry_u32getter(lines, container, container_id, route_stack):
 | 
			
		||||
    route_name = to_snake('_'.join([r['define'] for r in route_stack]))
 | 
			
		||||
    lines.append(f'        .u32getter = &{route_name}_getter,')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _append_routing_table_entry_const_data(lines, container, container_id, route_stack):
 | 
			
		||||
    route_name = to_snake('_'.join([r['define'] for r in route_stack]))
 | 
			
		||||
    lines.append(f'        .const_data = &{route_name}_data,')
 | 
			
		||||
    lines.append(f'        .const_data_len = sizeof({route_name}_data),')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _append_routing_table_entry_string(lines, container, container_id, route_stack):
 | 
			
		||||
    route_name = to_snake('_'.join([r['define'] for r in route_stack]))
 | 
			
		||||
    lines.append(f'        .const_data = {route_name}_str,')
 | 
			
		||||
    lines.append(f'        .const_data_len = sizeof({route_name}_str) - 1,')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _append_routing_table_entry(lines, container, container_id, route_stack):
 | 
			
		||||
    route_stack.append(container)
 | 
			
		||||
    route_name = '_'.join([r['define'] for r in route_stack])
 | 
			
		||||
    condition = route_conditions(route_stack)
 | 
			
		||||
 | 
			
		||||
    if condition:
 | 
			
		||||
        lines.append(f'#if {condition}')
 | 
			
		||||
 | 
			
		||||
    lines.append(f'    [{route_name}] = {{')
 | 
			
		||||
 | 
			
		||||
    _append_routing_table_entry_flags(lines, container, container_id, route_stack)
 | 
			
		||||
    if 'routes' in container:
 | 
			
		||||
        _append_routing_table_entry_route(lines, container, container_id, route_stack)
 | 
			
		||||
    elif 'return_constant' in container:
 | 
			
		||||
        if container['return_type'] == 'u32':
 | 
			
		||||
            _append_routing_table_entry_u32value(lines, container, container_id, route_stack)
 | 
			
		||||
        elif container['return_type'] == 'struct':
 | 
			
		||||
            _append_routing_table_entry_const_data(lines, container, container_id, route_stack)
 | 
			
		||||
        elif container['return_type'] == 'string':
 | 
			
		||||
            _append_routing_table_entry_string(lines, container, container_id, route_stack)
 | 
			
		||||
    elif 'return_getter' in container:
 | 
			
		||||
        if container['return_type'] == 'u32':
 | 
			
		||||
            _append_routing_table_entry_u32getter(lines, container, container_id, route_stack)
 | 
			
		||||
 | 
			
		||||
    lines.append(f'    }},')
 | 
			
		||||
 | 
			
		||||
    if condition:
 | 
			
		||||
        lines.append(f'#endif  // {condition}')
 | 
			
		||||
 | 
			
		||||
    route_stack.pop()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _append_routing_tables(lines, container, container_id=None, route_stack=None):
 | 
			
		||||
    """Handles building the list of the XAP routes, combining parent and child names together, as well as the route number.
 | 
			
		||||
    """
 | 
			
		||||
    if route_stack is None:
 | 
			
		||||
        route_stack = [container]
 | 
			
		||||
    else:
 | 
			
		||||
        route_stack.append(container)
 | 
			
		||||
 | 
			
		||||
    route_name = to_snake('_'.join([r['define'] for r in route_stack]))
 | 
			
		||||
    condition = route_conditions(route_stack)
 | 
			
		||||
 | 
			
		||||
    if 'routes' in container:
 | 
			
		||||
        for route_id in container['routes']:
 | 
			
		||||
            route = container['routes'][route_id]
 | 
			
		||||
            _append_routing_tables(lines, route, route_id, route_stack)
 | 
			
		||||
 | 
			
		||||
        for route_id in container['routes']:
 | 
			
		||||
            route = container['routes'][route_id]
 | 
			
		||||
            _append_routing_table_declaration(lines, route, route_id, route_stack)
 | 
			
		||||
 | 
			
		||||
        lines.append('')
 | 
			
		||||
        if condition:
 | 
			
		||||
            lines.append(f'#if {condition}')
 | 
			
		||||
 | 
			
		||||
        lines.append(f'static const xap_route_t {route_name}_table[] PROGMEM = {{')
 | 
			
		||||
 | 
			
		||||
        for route_id in container['routes']:
 | 
			
		||||
            route = container['routes'][route_id]
 | 
			
		||||
            _append_routing_table_entry(lines, route, route_id, route_stack)
 | 
			
		||||
 | 
			
		||||
        lines.append('};')
 | 
			
		||||
 | 
			
		||||
        if condition:
 | 
			
		||||
            lines.append(f'#endif  // {condition}')
 | 
			
		||||
            lines.append('')
 | 
			
		||||
 | 
			
		||||
    route_stack.pop()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_inline(output_file):
 | 
			
		||||
    """Generates the XAP protocol header file, generated during normal build.
 | 
			
		||||
    """
 | 
			
		||||
    xap_defs = latest_xap_defs()
 | 
			
		||||
 | 
			
		||||
    # Preamble
 | 
			
		||||
    lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '']
 | 
			
		||||
 | 
			
		||||
    # Add all the generated code
 | 
			
		||||
    _append_routing_tables(lines, xap_defs)
 | 
			
		||||
 | 
			
		||||
    # Generate the full output
 | 
			
		||||
    xap_generated_inl = '\n'.join(lines)
 | 
			
		||||
 | 
			
		||||
    # Clean up newlines
 | 
			
		||||
    while "\n\n\n" in xap_generated_inl:
 | 
			
		||||
        xap_generated_inl = xap_generated_inl.replace("\n\n\n", "\n\n")
 | 
			
		||||
 | 
			
		||||
    if output_file:
 | 
			
		||||
        if output_file.name == '-':
 | 
			
		||||
            print(xap_generated_inl)
 | 
			
		||||
        else:
 | 
			
		||||
            output_file.parent.mkdir(parents=True, exist_ok=True)
 | 
			
		||||
            if output_file.exists():
 | 
			
		||||
                output_file.replace(output_file.parent / (output_file.name + '.bak'))
 | 
			
		||||
            output_file.write_text(xap_generated_inl)
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue