TouchPortalAPI v1.7.8
TouchPortalAPI.tppdoc
TouchPortal Python TPP documentation generator
Features
This SDK tools will generate a markdown file that can be used to document your TouchPortal plugin.
This is what it includes:
- automatically generate a table of contents
- automatically generate a badges that shows total downloads, forks, stars and license
- automatically generate action section and show details of each data
- automatically generate connector section and show details of each data
- automatically generate state section
- automatically generate event section
- automatically generate settings section
- automatically generate installation section (if you include
"doc": {"install": ""})
inTP_PLUGIN_INFO
) - automatically generate bugs and support section
Using it in example
tppdoc plugin_example.py
In this example we are using plugin_example.py
file because that file contains entry infomations and using those information we can generate a markdown file.
Command-line Usage
The script command is tppdoc
when the TouchPortalAPI is installed (via pip or setup), or tppdoc.py
when run directly from this source.
<script-command> [-h] [-i] [-o OUTPUT] <target>
Script to automatically generates a documentation for a TouchPortal plugin.
positional arguments:
<target> tppdoc is a documanentation generator for TouchPortal plugins. It uses py entry to generates a markdown file.It
can generate a table for the plugin settings, connectors, actions, state, and event. and It will also show data
field example, It can show max length, min value, max value for the data field.
options:
-h, --help show this help message and exit
-i, --ignoreError Ignore error when validating. Default is False.
-o OUTPUT, --output OUTPUT Name of generated documentation. Default is "Documentation". You do not need to add the extension.
1""" 2# TouchPortal Python TPP documentation generator 3 4## Features 5 6This SDK tools will generate a markdown file that can be used to document your TouchPortal plugin. 7 8This is what it includes: 9- automatically generate a table of contents 10- automatically generate a badges that shows total downloads, forks, stars and license 11- automatically generate action section and show details of each data 12- automatically generate connector section and show details of each data 13- automatically generate state section 14- automatically generate event section 15- automatically generate settings section 16- automatically generate installation section (if you include `"doc": {"install": ""})` in `TP_PLUGIN_INFO`) 17- automatically generate bugs and support section 18 19Using it in [example](https://github.com/KillerBOSS2019/TouchPortal-API/tree/main/examples) 20 21``` 22tppdoc plugin_example.py 23``` 24In this example we are using `plugin_example.py` file because that file contains entry infomations and using those information we can generate a markdown file. 25 26## Command-line Usage 27The script command is `tppdoc` when the TouchPortalAPI is installed (via pip or setup), or `tppdoc.py` when run directly from this source. 28 29``` 30<script-command> [-h] [-i] [-o OUTPUT] <target> 31 32Script to automatically generates a documentation for a TouchPortal plugin. 33 34positional arguments: 35 <target> tppdoc is a documanentation generator for TouchPortal plugins. It uses py entry to generates a markdown file.It 36 can generate a table for the plugin settings, connectors, actions, state, and event. and It will also show data 37 field example, It can show max length, min value, max value for the data field. 38 39options: 40 -h, --help show this help message and exit 41 -i, --ignoreError Ignore error when validating. Default is False. 42 -o OUTPUT, --output OUTPUT Name of generated documentation. Default is "Documentation". You do not need to add the extension. 43``` 44""" 45 46__copyright__ = """ 47This file is part of the TouchPortal-API project. 48Copyright TouchPortal-API Developers 49Copyright (c) 2021 DamienS 50All rights reserved. 51 52This program is free software: you can redistribute it and/or modify 53it under the terms of the GNU General Public License as published by 54the Free Software Foundation, either version 3 of the License, or 55(at your option) any later version. 56 57This program is distributed in the hope that it will be useful, 58but WITHOUT ANY WARRANTY; without even the implied warranty of 59MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 60GNU General Public License for more details. 61 62You should have received a copy of the GNU General Public License 63along with this program. If not, see <https://www.gnu.org/licenses/>. 64""" 65 66import sys, os 67import importlib 68from argparse import ArgumentParser 69 70sys.path.insert(0, os.path.dirname(os.path.realpath(__file__))) 71from sdk_tools import _validateDefinition, generateDefinitionFromScript 72import TpToPy 73 74def getInfoFromBuildScript(script:str): 75 try: 76 sys.path.insert(1, os.getcwd()) # This allows build config to import stuff 77 spec = importlib.util.spec_from_file_location("entry", script) 78 entry = importlib.util.module_from_spec(spec) 79 spec.loader.exec_module(entry) 80 except Exception as e: 81 raise ImportError(f"ERROR while trying to import entry code from '{script}': {repr(e)}") 82 return entry 83 84def generateCategoryLink(linkType, entry, categoryStruct): 85 linkList = [] 86 # Ik this is not the most efficient way to do it 87 for category in categoryStruct: 88 for item in entry: 89 if (theItem := entry[item].get('category')) == category: 90 linkName = categoryStruct[theItem].get('name') 91 linkAdress = categoryStruct[theItem].get('id') + linkType 92 if (dataToAppend := f"\n - [{linkName}](#{linkAdress})") not in linkList: 93 linkList.append(dataToAppend) 94 95 # print(linkList) 96 return "".join(linkList) 97 98def generateTableContent(entry, entryFile): 99 table_content = f""" 100# {entry['name'].replace(" ", "-")}""" 101 if entry.get('doc') and (repository := entry['doc'].get("repository")) and repository.find(":") != -1: 102 table_content += f""" 103![Downloads](https://img.shields.io/github/downloads/{entry['doc']['repository'].split(":")[0]}/{entry['doc']['repository'].split(":")[1]}/total) 104![Forks](https://img.shields.io/github/forks/{entry['doc']['repository'].split(":")[0]}/{entry['doc']['repository'].split(":")[1]}) 105![Stars](https://img.shields.io/github/stars/{entry['doc']['repository'].split(":")[0]}/{entry['doc']['repository'].split(":")[1]}) 106![License](https://img.shields.io/github/license/{entry['doc']['repository'].split(":")[0]}/{entry['doc']['repository'].split(":")[1]}) 107""" 108 109 table_content += f""" 110- [{entry['name']}](#{entry['name'].replace(" ", "-")}) 111 - [Description](#description)""" 112 if hasattr(entryFile, "TP_PLUGIN_SETTINGS") and entryFile.TP_PLUGIN_SETTINGS: 113 table_content += """ \n - [Settings Overview](#Settings-Overview)""" 114 115 table_content += """ 116 - [Features](#Features)""" 117 118 if "TP_PLUGIN_ACTIONS" in dir(entryFile) and entryFile.TP_PLUGIN_ACTIONS: 119 table_content += """\n - [Actions](#actions)""" 120 121 table_content += generateCategoryLink("actions", entryFile.TP_PLUGIN_ACTIONS, entryFile.TP_PLUGIN_CATEGORIES) 122 123 if "TP_PLUGIN_CONNECTORS" in dir(entryFile) and entryFile.TP_PLUGIN_CONNECTORS: 124 table_content += """ 125 - [Connectors](#connectors)""" 126 table_content += generateCategoryLink("connectors", entryFile.TP_PLUGIN_CONNECTORS, entryFile.TP_PLUGIN_CATEGORIES) 127 128 if "TP_PLUGIN_STATES" in dir(entryFile) and entryFile.TP_PLUGIN_STATES: 129 table_content += """ 130 - [States](#states)""" 131 table_content += generateCategoryLink("states", entryFile.TP_PLUGIN_STATES, entryFile.TP_PLUGIN_CATEGORIES) 132 133 if "TP_PLUGIN_EVENTS" in dir(entryFile) and entryFile.TP_PLUGIN_EVENTS: 134 table_content += """ 135 - [Events](#events)""" 136 table_content += generateCategoryLink("events", entryFile.TP_PLUGIN_EVENTS, entryFile.TP_PLUGIN_CATEGORIES) 137 138 if entry.get("doc") and entry['doc'].get("Install"): 139 table_content += """ 140 - [Installation Guide](#installation)""" 141 142 table_content += """ 143 - [Bugs and Support](#bugs-and-suggestion) 144 - [License](#license) 145 """ 146 return table_content 147 148def typeNumber(entry): 149 typeDoc = "" 150 if entry.get('minValue'): 151 typeDoc += f" <b>Min Value:</b> {entry['minValue']}" 152 else: 153 typeDoc += " <b>Min Value:</b> -2147483648" 154 155 if entry.get('maxValue'): 156 typeDoc += f" <b>Max Value:</b> {entry['maxValue']}" 157 else: 158 typeDoc += f" <b>Max Value:</b> 2147483647" 159 160 if entry.get('allowDecimals'): 161 typeDoc += f" <b>Allow Decimals:</b> {entry['allowDecimals']}" 162 163 return typeDoc 164 165def __generateData(entry): 166 dataDocList = "" 167 needDropdown = False 168 if entry.get('data'): 169 dataDocList += "<td><ol start=1>" 170 if len(entry['data']) > 3: 171 dataDocList = "<td><details><summary><ins>Click to expand</ins></summary><ol start=1>\n" 172 needDropdown = True 173 for data in entry['data']: 174 dataDocList += f"<li>Type: {entry['data'][data]['type']} \n" 175 176 if entry['data'][data]['type'] == "choice" and entry['data'][data].get('valueChoices'): 177 dataDocList += f"Default: <b>{entry['data'][data]['default']}</b> Possible choices: {entry['data'][data]['valueChoices']}" 178 elif "default" in entry['data'][data].keys() and entry['data'][data]['default'] != "": 179 dataDocList += f"Default: <b>{entry['data'][data]['default']}</b>" 180 else: 181 dataDocList += "<empty>" 182 183 if entry['data'][data]['type'] == "number": 184 dataDocList += typeNumber(entry['data'][data]) 185 186 dataDocList += "</li>\n" 187 dataDocList += "</ol></td>\n" 188 if needDropdown: 189 dataDocList += "</details>" 190 else: 191 dataDocList += "<td> </td>\n" 192 193 return dataDocList 194 195def getCategoryName(categoryId, categoryStruct): 196 try: 197 return categoryStruct[categoryId].get('name', categoryId) 198 except IndexError: 199 return categoryStruct[list(categoryStruct.keys())[0]] # If it does not have a `category` field It will use default 200 201def getCategoryId(categoryId, categoryStruct): 202 return categoryStruct.get(categoryId) 203 204def generateAction(entry, categoryStruct): 205 actionDoc = "\n## Actions\n" 206 filterActionbyCategory = {} 207 208 numberOfCategory = [entry[x].get("category", "main") for x in entry] 209 allowDetailOpen = not len(set(numberOfCategory)) > 1 210 211 for action in entry: 212 categoryName = entry[action].get("category", "main") 213 if entry[action]['category'] not in filterActionbyCategory: 214 filterActionbyCategory[categoryName] = "<table>\n" 215 filterActionbyCategory[categoryName] += "<tr valign='buttom'>" + "<th>Action Name</th>" + "<th>Description</th>" + "<th>Format</th>" + \ 216 "<th nowrap>Data<br/><div align=left><sub>choices/default (in bold)</th>" + \ 217 "<th>On<br/>Hold</sub></div></th>" + \ 218 "</tr>\n" 219 220 filterActionbyCategory[categoryName] += f"<tr valign='top'><td>{entry[action]['name']}</td>" + \ 221 f"<td>{entry[action]['doc'] if entry[action].get('doc') else ' '}</td>" + \ 222 f"<td>{entry[action]['format'].replace('$', '') if entry[action].get('format') else ' '}</td>" 223 filterActionbyCategory[categoryName] += __generateData(entry[action]) 224 225 filterActionbyCategory[categoryName] += f"<td align=center>{'Yes' if entry[action].get('hasHoldFunctionality') and entry[action]['hasHoldFunctionality'] else 'No'}</td>\n" 226 227 for category in filterActionbyCategory: 228 categoryRealName = getCategoryName(categoryId=category, categoryStruct=categoryStruct) 229 categoryLinkAddress = getCategoryId(category, categoryStruct).get("id") + "actions" # to make it unique 230 actionDoc += f"<details {'open' if list(filterActionbyCategory.keys()).index(category) == 0 and allowDetailOpen else ''} id='{categoryLinkAddress}'><summary><b>Category:</b> {categoryRealName} <small><ins>(Click to expand)</ins></small></summary>" 231 actionDoc += filterActionbyCategory[category] 232 actionDoc += "</tr></table></details>\n" 233 234 actionDoc += "<br>\n" 235 return actionDoc 236 237def generateConnectors(entry, categoryStruct): 238 connectorDoc = "\n## Connectors\n" 239 filterConnectorsbyCategory = {} 240 241 numberOfCategory = [entry[x].get("category", "main") for x in entry] 242 allowDetailOpen = not len(set(numberOfCategory)) > 1 243 244 for connector in entry: 245 categoryName = entry[connector].get("category", "main") 246 if entry[connector]['category'] not in filterConnectorsbyCategory: 247 filterConnectorsbyCategory[categoryName] = "<table>\n" 248 filterConnectorsbyCategory[categoryName] += "<tr valign='buttom'>" + "<th>Slider Name</th>" + "<th>Description</th>" + "<th>Format</th>" + \ 249 "<th nowrap>Data<br/><div align=left><sub>choices/default (in bold)</th>" + "</tr>\n" 250 251 filterConnectorsbyCategory[categoryName] += f"<tr valign='top'><td>{entry[connector]['name']}</td>" + \ 252 f"<td>{entry[connector]['doc'] if entry[connector].get('doc') else ' '}</td>" + \ 253 f"<td>{entry[connector]['format'].replace('$', '') if entry[connector].get('format') else ' '}</td>" 254 255 filterConnectorsbyCategory[categoryName] += __generateData(entry[connector]) 256 257 for category in filterConnectorsbyCategory: 258 categoryRealName = getCategoryName(categoryId=category, categoryStruct=categoryStruct) 259 categoryLinkAddress = getCategoryId(category, categoryStruct).get("id") + "connectors" 260 connectorDoc += f"<details {'open' if list(filterConnectorsbyCategory.keys()).index(category) == 0 and allowDetailOpen else ''} id='{categoryLinkAddress}'><summary><b>Category:</b> {categoryRealName} <small><ins>(Click to expand)</ins></small></summary>" 261 connectorDoc += filterConnectorsbyCategory[category] 262 connectorDoc += "</table></details>\n" 263 connectorDoc += "<br>\n" 264 265 return connectorDoc 266 267 268 269def generateSetting(entry): 270 settingDoc = "\n\n## Settings Overview\n" 271 272 def f(data): 273 return [data.get('maxLength', 0) > 0, data.get('minValue', None), data.get('maxValue', None)] 274 275 for setting in entry.keys(): 276 settingDoc += "| Read-only | Type | Default Value" 277 if f(entry[setting])[0]: settingDoc += f" | Max. Length" 278 if f(entry[setting])[1]: settingDoc += f" | Min. Value" 279 if f(entry[setting])[2]: settingDoc += f" | Max. Value" 280 settingDoc += " |\n" 281 settingDoc += "| --- | --- | ---" 282 if f(entry[setting])[0]: settingDoc += f" | ---" 283 if f(entry[setting])[1]: settingDoc += f" | ---" 284 if f(entry[setting])[2]: settingDoc += f" | ---" 285 286 settingDoc += " |\n" 287 settingDoc += f"| {entry[setting].get('readOnly', False)} | {entry[setting]['type']} | {entry[setting]['default']}" 288 if f(entry[setting])[0]: settingDoc += f" | {entry[setting]['maxLength']}" 289 if f(entry[setting])[1]: settingDoc += f" | {entry[setting]['minValue']}" 290 if f(entry[setting])[2]: settingDoc += f" | {entry[setting]['maxValue']}" 291 settingDoc += " |\n\n" 292 if entry[setting].get('doc'): 293 settingDoc += f"{entry[setting]['doc']}\n\n" 294 return settingDoc 295 296def generateState(entry, baseid, categoryStruct): 297 stateDoc = "\n## States\n" 298 filterCategory = {} 299 300 numberOfCategory = [entry[x].get("category", "main") for x in entry] 301 allowDetailOpen = not len(set(numberOfCategory)) > 1 302 303 for state in entry: 304 categoryName = entry[state].get("category", "main") 305 state = entry[state] 306 if not categoryName in filterCategory: 307 categoryRealName = getCategoryName(categoryId=state.get('category'), categoryStruct=categoryStruct) 308 categoryLinkAddress = getCategoryId(state.get('category'), categoryStruct).get("id") + "states" 309 filterCategory[categoryName] = "" 310 filterCategory[categoryName] += f"<details{' open' if len(filterCategory) == 1 and allowDetailOpen else ''} id='{categoryLinkAddress}'><summary><b>Category:</b> {categoryRealName} <small><ins>(Click to expand)</ins></small></summary>\n" 311 filterCategory[categoryName] += "\n\n| Id | Description | DefaultValue | parentGroup |\n" 312 filterCategory[categoryName] += "| --- | --- | --- | --- |\n" 313 314 filterCategory[categoryName] += f"| {state['id'].split(baseid)[-1]} | {state['desc']} | {state['default']} | {state.get('parentGroup', ' ')} |\n" 315 316 for category in filterCategory: 317 stateDoc += filterCategory[category] 318 stateDoc += "</details>\n\n" 319 stateDoc += "<br>\n" 320 321 return stateDoc 322 323def generateEvent(entry, baseid, categoryStruct): 324 eventDoc = "\n## Events\n\n" 325 filterCategory = {} 326 numberOfCategory = [entry[x].get("category", "main") for x in entry] 327 allowDetailOpen = not len(set(numberOfCategory)) > 1 328 for event in entry: 329 event = entry[event] # dict looks like {'0': {}, '1': {}}. so when looping It will give `0` etc.. 330 needDropdown = False 331 332 categoryName = event.get("category", "main") 333 if not categoryName in filterCategory: 334 categoryRealName = getCategoryName(categoryId=categoryName, categoryStruct=categoryStruct) 335 categoryLinkAddress = getCategoryId(categoryName, categoryStruct).get("id") + "events" 336 filterCategory[categoryName] = "" 337 filterCategory[categoryName] += f"<details{' open' if len(filterCategory) == 1 and allowDetailOpen else ''} id='{categoryLinkAddress}'><summary><b>Category: </b>{categoryRealName} <small><ins>(Click to expand)</ins></small></summary>\n\n" 338 filterCategory[categoryName] += "<table>\n" 339 filterCategory[categoryName] += "<tr valign='buttom'>" + "<th>Id</th>" + "<th>Name</th>" + "<th nowrap>Evaluated State Id</th>" + \ 340 "<th>Format</th>" + "<th>Type</th>" + "<th>Choice(s)</th>" + "</tr>\n" 341 342 filterCategory[categoryName] += f"<tr valign='top'><td>{event['id'].split(baseid)[-1]}</td>" + \ 343 f"<td>{event.get('name', '')}</td>" + \ 344 f"<td>{event.get('valueStateId', '').split(baseid)[-1]}</td>" + \ 345 f"<td>{event.get('format', '')}</td>" + \ 346 f"<td>{event.get('valueType', '')}</td>" + \ 347 "<td>" 348 349 if len(event.get('valueChoices', [])) > 5: 350 filterCategory[categoryName] += f"<details><summary><ins>detail</ins></summary>\n" 351 needDropdown = True 352 353 filterCategory[categoryName] += f"<ul>" 354 for item in event.get('valueChoices', []): 355 filterCategory[categoryName] += f"<li>{item}</li>" 356 filterCategory[categoryName] += "</ul></td>" 357 358 if needDropdown: 359 filterCategory[categoryName] += "</details>" 360 eventDoc += "<td></tr>\n" 361 362 for category in filterCategory: 363 eventDoc += filterCategory[category] 364 eventDoc += f"</table></details>\n" 365 eventDoc += "<br>\n" 366 367 return eventDoc 368 369def main(docArg=None): 370 parser = ArgumentParser(description= 371 "Script to automatically generates a documentation for a TouchPortal plugin.") 372 373 parser.add_argument( 374 "target", metavar='<target>', type=str, 375 help='tppdoc is a documanentation generator for TouchPortal plugins. It uses py entry to generates a markdown file.' + \ 376 'It can generate a table for the plugin settings, connectors, actions, state, and event. and It will also show' + \ 377 ' data field example, It can show max length, min value, max value for the data field.' 378 ) 379 380 parser.add_argument( 381 "-i", "--ignoreError", action='store_true', default=False, 382 help='Ignore error when validating. Default is False.' 383 ) 384 385 parser.add_argument( 386 "-o", "--output", default="Documentation.md", 387 help='Name of generated documentation. Default is "Documentation". You do not need to add the extension.' 388 ) 389 390 opts = parser.parse_args(docArg) 391 del parser 392 393 out_dir = os.path.dirname(opts.target) 394 targetPathbaseName = os.path.basename(opts.target) 395 396 if out_dir: 397 os.chdir(out_dir) 398 399 entryType = "py" if targetPathbaseName.endswith(".py") else "tp" 400 if not opts.ignoreError: 401 print("vaildating entry...\n") 402 if entryType == "tp" and _validateDefinition(targetPathbaseName): 403 print(targetPathbaseName, "is vaild file. continue building document.\n") 404 elif entryType == "py" and _validateDefinition(generateDefinitionFromScript(targetPathbaseName), as_str=True): 405 print(targetPathbaseName, "is vaild file. continue building document.\n") 406 else: 407 print("File is invalid. Please above error for more information.") 408 return -1 409 else: 410 print("Ignoring errors, contiune building document.\n") 411 412 if entryType == "py": entry = getInfoFromBuildScript(targetPathbaseName) 413 else: entry = TpToPy.toString(targetPathbaseName) 414 415 416 documentation = """""" 417 418 print("Building table of content\n") 419 tableContent = generateTableContent(entry.TP_PLUGIN_INFO, entry) 420 documentation += tableContent 421 422 documentation += f""" 423# Description 424 425""" 426 if entry.TP_PLUGIN_INFO.get('doc') and entry.TP_PLUGIN_INFO['doc'].get('description'): 427 documentation += f"{entry.TP_PLUGIN_INFO['doc']['description']}\n\n" 428 429 documentation += f"This documentation generated for {entry.TP_PLUGIN_INFO['name']} V{entry.TP_PLUGIN_INFO['version']} with [Python TouchPortal SDK](https://github.com/KillerBOSS2019/TouchPortal-API)." 430 if entry.TP_PLUGIN_SETTINGS: 431 print("Generating settings section\n") 432 setting = generateSetting(entry.TP_PLUGIN_SETTINGS) 433 documentation += setting 434 435 documentation += "\n# Features\n" 436 categoryStruct = entry.TP_PLUGIN_CATEGORIES 437 438 if "TP_PLUGIN_ACTIONS" in dir(entry) and entry.TP_PLUGIN_ACTIONS: 439 print("Generating action section\n") 440 action = generateAction(entry.TP_PLUGIN_ACTIONS, categoryStruct) 441 documentation += action 442 443 444 if "TP_PLUGIN_CONNECTORS" in dir(entry) and entry.TP_PLUGIN_CONNECTORS: 445 print("Generating connector section\n") 446 connector = generateConnectors(entry.TP_PLUGIN_CONNECTORS, categoryStruct) 447 documentation += connector 448 449 if "TP_PLUGIN_STATES" in dir(entry) and entry.TP_PLUGIN_STATES: 450 print("Generating state section\n") 451 state = generateState(entry.TP_PLUGIN_STATES, entry.TP_PLUGIN_INFO['id'], categoryStruct) 452 documentation += state 453 454 if "TP_PLUGIN_EVENTS" in dir(entry) and entry.TP_PLUGIN_EVENTS: 455 print("Generating event section\n") 456 event = generateEvent(entry.TP_PLUGIN_EVENTS, entry.TP_PLUGIN_INFO['id'], categoryStruct) 457 documentation += event 458 459 if entry.TP_PLUGIN_INFO.get("doc") and entry.TP_PLUGIN_INFO['doc'].get('Install'): 460 print("Found install method. Generating install section\n") 461 documentation += "\n# Installation\n" 462 documentation += entry.TP_PLUGIN_INFO['doc']['Install'] 463 464 print("Generating Bugs and Suggestion section\n") 465 documentation += "\n# Bugs and Suggestion\n" 466 try: 467 documentation += f"Open an [issue](https://github.com/{'/'.join(entry.TP_PLUGIN_INFO['doc']['repository'].split(':'))}/issues) or join offical [TouchPortal Discord](https://discord.gg/MgxQb8r) for support.\n\n" 468 except: 469 documentation += f"Open an issue on github or join offical [TouchPortal Discord](https://discord.gg/MgxQb8r) for support.\n\n" 470 471 documentation += "\n# License\n" 472 documentation += "This plugin is licensed under the [GPL 3.0 License] - see the [LICENSE](LICENSE) file for more information.\n\n" 473 474 with open(opts.output, "w") as f: 475 f.write(documentation) 476 477 print("Finished generating documentation.") 478 return 0 479 480if __name__ == "__main__": 481 sys.exit(main())
def
getInfoFromBuildScript(script: str)
75def getInfoFromBuildScript(script:str): 76 try: 77 sys.path.insert(1, os.getcwd()) # This allows build config to import stuff 78 spec = importlib.util.spec_from_file_location("entry", script) 79 entry = importlib.util.module_from_spec(spec) 80 spec.loader.exec_module(entry) 81 except Exception as e: 82 raise ImportError(f"ERROR while trying to import entry code from '{script}': {repr(e)}") 83 return entry
def
generateCategoryLink(linkType, entry, categoryStruct)
85def generateCategoryLink(linkType, entry, categoryStruct): 86 linkList = [] 87 # Ik this is not the most efficient way to do it 88 for category in categoryStruct: 89 for item in entry: 90 if (theItem := entry[item].get('category')) == category: 91 linkName = categoryStruct[theItem].get('name') 92 linkAdress = categoryStruct[theItem].get('id') + linkType 93 if (dataToAppend := f"\n - [{linkName}](#{linkAdress})") not in linkList: 94 linkList.append(dataToAppend) 95 96 # print(linkList) 97 return "".join(linkList)
def
generateTableContent(entry, entryFile)
99def generateTableContent(entry, entryFile): 100 table_content = f""" 101# {entry['name'].replace(" ", "-")}""" 102 if entry.get('doc') and (repository := entry['doc'].get("repository")) and repository.find(":") != -1: 103 table_content += f""" 104![Downloads](https://img.shields.io/github/downloads/{entry['doc']['repository'].split(":")[0]}/{entry['doc']['repository'].split(":")[1]}/total) 105![Forks](https://img.shields.io/github/forks/{entry['doc']['repository'].split(":")[0]}/{entry['doc']['repository'].split(":")[1]}) 106![Stars](https://img.shields.io/github/stars/{entry['doc']['repository'].split(":")[0]}/{entry['doc']['repository'].split(":")[1]}) 107![License](https://img.shields.io/github/license/{entry['doc']['repository'].split(":")[0]}/{entry['doc']['repository'].split(":")[1]}) 108""" 109 110 table_content += f""" 111- [{entry['name']}](#{entry['name'].replace(" ", "-")}) 112 - [Description](#description)""" 113 if hasattr(entryFile, "TP_PLUGIN_SETTINGS") and entryFile.TP_PLUGIN_SETTINGS: 114 table_content += """ \n - [Settings Overview](#Settings-Overview)""" 115 116 table_content += """ 117 - [Features](#Features)""" 118 119 if "TP_PLUGIN_ACTIONS" in dir(entryFile) and entryFile.TP_PLUGIN_ACTIONS: 120 table_content += """\n - [Actions](#actions)""" 121 122 table_content += generateCategoryLink("actions", entryFile.TP_PLUGIN_ACTIONS, entryFile.TP_PLUGIN_CATEGORIES) 123 124 if "TP_PLUGIN_CONNECTORS" in dir(entryFile) and entryFile.TP_PLUGIN_CONNECTORS: 125 table_content += """ 126 - [Connectors](#connectors)""" 127 table_content += generateCategoryLink("connectors", entryFile.TP_PLUGIN_CONNECTORS, entryFile.TP_PLUGIN_CATEGORIES) 128 129 if "TP_PLUGIN_STATES" in dir(entryFile) and entryFile.TP_PLUGIN_STATES: 130 table_content += """ 131 - [States](#states)""" 132 table_content += generateCategoryLink("states", entryFile.TP_PLUGIN_STATES, entryFile.TP_PLUGIN_CATEGORIES) 133 134 if "TP_PLUGIN_EVENTS" in dir(entryFile) and entryFile.TP_PLUGIN_EVENTS: 135 table_content += """ 136 - [Events](#events)""" 137 table_content += generateCategoryLink("events", entryFile.TP_PLUGIN_EVENTS, entryFile.TP_PLUGIN_CATEGORIES) 138 139 if entry.get("doc") and entry['doc'].get("Install"): 140 table_content += """ 141 - [Installation Guide](#installation)""" 142 143 table_content += """ 144 - [Bugs and Support](#bugs-and-suggestion) 145 - [License](#license) 146 """ 147 return table_content
def
typeNumber(entry)
149def typeNumber(entry): 150 typeDoc = "" 151 if entry.get('minValue'): 152 typeDoc += f" <b>Min Value:</b> {entry['minValue']}" 153 else: 154 typeDoc += " <b>Min Value:</b> -2147483648" 155 156 if entry.get('maxValue'): 157 typeDoc += f" <b>Max Value:</b> {entry['maxValue']}" 158 else: 159 typeDoc += f" <b>Max Value:</b> 2147483647" 160 161 if entry.get('allowDecimals'): 162 typeDoc += f" <b>Allow Decimals:</b> {entry['allowDecimals']}" 163 164 return typeDoc
def
getCategoryName(categoryId, categoryStruct)
def
getCategoryId(categoryId, categoryStruct)
def
generateAction(entry, categoryStruct)
205def generateAction(entry, categoryStruct): 206 actionDoc = "\n## Actions\n" 207 filterActionbyCategory = {} 208 209 numberOfCategory = [entry[x].get("category", "main") for x in entry] 210 allowDetailOpen = not len(set(numberOfCategory)) > 1 211 212 for action in entry: 213 categoryName = entry[action].get("category", "main") 214 if entry[action]['category'] not in filterActionbyCategory: 215 filterActionbyCategory[categoryName] = "<table>\n" 216 filterActionbyCategory[categoryName] += "<tr valign='buttom'>" + "<th>Action Name</th>" + "<th>Description</th>" + "<th>Format</th>" + \ 217 "<th nowrap>Data<br/><div align=left><sub>choices/default (in bold)</th>" + \ 218 "<th>On<br/>Hold</sub></div></th>" + \ 219 "</tr>\n" 220 221 filterActionbyCategory[categoryName] += f"<tr valign='top'><td>{entry[action]['name']}</td>" + \ 222 f"<td>{entry[action]['doc'] if entry[action].get('doc') else ' '}</td>" + \ 223 f"<td>{entry[action]['format'].replace('$', '') if entry[action].get('format') else ' '}</td>" 224 filterActionbyCategory[categoryName] += __generateData(entry[action]) 225 226 filterActionbyCategory[categoryName] += f"<td align=center>{'Yes' if entry[action].get('hasHoldFunctionality') and entry[action]['hasHoldFunctionality'] else 'No'}</td>\n" 227 228 for category in filterActionbyCategory: 229 categoryRealName = getCategoryName(categoryId=category, categoryStruct=categoryStruct) 230 categoryLinkAddress = getCategoryId(category, categoryStruct).get("id") + "actions" # to make it unique 231 actionDoc += f"<details {'open' if list(filterActionbyCategory.keys()).index(category) == 0 and allowDetailOpen else ''} id='{categoryLinkAddress}'><summary><b>Category:</b> {categoryRealName} <small><ins>(Click to expand)</ins></small></summary>" 232 actionDoc += filterActionbyCategory[category] 233 actionDoc += "</tr></table></details>\n" 234 235 actionDoc += "<br>\n" 236 return actionDoc
def
generateConnectors(entry, categoryStruct)
238def generateConnectors(entry, categoryStruct): 239 connectorDoc = "\n## Connectors\n" 240 filterConnectorsbyCategory = {} 241 242 numberOfCategory = [entry[x].get("category", "main") for x in entry] 243 allowDetailOpen = not len(set(numberOfCategory)) > 1 244 245 for connector in entry: 246 categoryName = entry[connector].get("category", "main") 247 if entry[connector]['category'] not in filterConnectorsbyCategory: 248 filterConnectorsbyCategory[categoryName] = "<table>\n" 249 filterConnectorsbyCategory[categoryName] += "<tr valign='buttom'>" + "<th>Slider Name</th>" + "<th>Description</th>" + "<th>Format</th>" + \ 250 "<th nowrap>Data<br/><div align=left><sub>choices/default (in bold)</th>" + "</tr>\n" 251 252 filterConnectorsbyCategory[categoryName] += f"<tr valign='top'><td>{entry[connector]['name']}</td>" + \ 253 f"<td>{entry[connector]['doc'] if entry[connector].get('doc') else ' '}</td>" + \ 254 f"<td>{entry[connector]['format'].replace('$', '') if entry[connector].get('format') else ' '}</td>" 255 256 filterConnectorsbyCategory[categoryName] += __generateData(entry[connector]) 257 258 for category in filterConnectorsbyCategory: 259 categoryRealName = getCategoryName(categoryId=category, categoryStruct=categoryStruct) 260 categoryLinkAddress = getCategoryId(category, categoryStruct).get("id") + "connectors" 261 connectorDoc += f"<details {'open' if list(filterConnectorsbyCategory.keys()).index(category) == 0 and allowDetailOpen else ''} id='{categoryLinkAddress}'><summary><b>Category:</b> {categoryRealName} <small><ins>(Click to expand)</ins></small></summary>" 262 connectorDoc += filterConnectorsbyCategory[category] 263 connectorDoc += "</table></details>\n" 264 connectorDoc += "<br>\n" 265 266 return connectorDoc
def
generateSetting(entry)
270def generateSetting(entry): 271 settingDoc = "\n\n## Settings Overview\n" 272 273 def f(data): 274 return [data.get('maxLength', 0) > 0, data.get('minValue', None), data.get('maxValue', None)] 275 276 for setting in entry.keys(): 277 settingDoc += "| Read-only | Type | Default Value" 278 if f(entry[setting])[0]: settingDoc += f" | Max. Length" 279 if f(entry[setting])[1]: settingDoc += f" | Min. Value" 280 if f(entry[setting])[2]: settingDoc += f" | Max. Value" 281 settingDoc += " |\n" 282 settingDoc += "| --- | --- | ---" 283 if f(entry[setting])[0]: settingDoc += f" | ---" 284 if f(entry[setting])[1]: settingDoc += f" | ---" 285 if f(entry[setting])[2]: settingDoc += f" | ---" 286 287 settingDoc += " |\n" 288 settingDoc += f"| {entry[setting].get('readOnly', False)} | {entry[setting]['type']} | {entry[setting]['default']}" 289 if f(entry[setting])[0]: settingDoc += f" | {entry[setting]['maxLength']}" 290 if f(entry[setting])[1]: settingDoc += f" | {entry[setting]['minValue']}" 291 if f(entry[setting])[2]: settingDoc += f" | {entry[setting]['maxValue']}" 292 settingDoc += " |\n\n" 293 if entry[setting].get('doc'): 294 settingDoc += f"{entry[setting]['doc']}\n\n" 295 return settingDoc
def
generateState(entry, baseid, categoryStruct)
297def generateState(entry, baseid, categoryStruct): 298 stateDoc = "\n## States\n" 299 filterCategory = {} 300 301 numberOfCategory = [entry[x].get("category", "main") for x in entry] 302 allowDetailOpen = not len(set(numberOfCategory)) > 1 303 304 for state in entry: 305 categoryName = entry[state].get("category", "main") 306 state = entry[state] 307 if not categoryName in filterCategory: 308 categoryRealName = getCategoryName(categoryId=state.get('category'), categoryStruct=categoryStruct) 309 categoryLinkAddress = getCategoryId(state.get('category'), categoryStruct).get("id") + "states" 310 filterCategory[categoryName] = "" 311 filterCategory[categoryName] += f"<details{' open' if len(filterCategory) == 1 and allowDetailOpen else ''} id='{categoryLinkAddress}'><summary><b>Category:</b> {categoryRealName} <small><ins>(Click to expand)</ins></small></summary>\n" 312 filterCategory[categoryName] += "\n\n| Id | Description | DefaultValue | parentGroup |\n" 313 filterCategory[categoryName] += "| --- | --- | --- | --- |\n" 314 315 filterCategory[categoryName] += f"| {state['id'].split(baseid)[-1]} | {state['desc']} | {state['default']} | {state.get('parentGroup', ' ')} |\n" 316 317 for category in filterCategory: 318 stateDoc += filterCategory[category] 319 stateDoc += "</details>\n\n" 320 stateDoc += "<br>\n" 321 322 return stateDoc
def
generateEvent(entry, baseid, categoryStruct)
324def generateEvent(entry, baseid, categoryStruct): 325 eventDoc = "\n## Events\n\n" 326 filterCategory = {} 327 numberOfCategory = [entry[x].get("category", "main") for x in entry] 328 allowDetailOpen = not len(set(numberOfCategory)) > 1 329 for event in entry: 330 event = entry[event] # dict looks like {'0': {}, '1': {}}. so when looping It will give `0` etc.. 331 needDropdown = False 332 333 categoryName = event.get("category", "main") 334 if not categoryName in filterCategory: 335 categoryRealName = getCategoryName(categoryId=categoryName, categoryStruct=categoryStruct) 336 categoryLinkAddress = getCategoryId(categoryName, categoryStruct).get("id") + "events" 337 filterCategory[categoryName] = "" 338 filterCategory[categoryName] += f"<details{' open' if len(filterCategory) == 1 and allowDetailOpen else ''} id='{categoryLinkAddress}'><summary><b>Category: </b>{categoryRealName} <small><ins>(Click to expand)</ins></small></summary>\n\n" 339 filterCategory[categoryName] += "<table>\n" 340 filterCategory[categoryName] += "<tr valign='buttom'>" + "<th>Id</th>" + "<th>Name</th>" + "<th nowrap>Evaluated State Id</th>" + \ 341 "<th>Format</th>" + "<th>Type</th>" + "<th>Choice(s)</th>" + "</tr>\n" 342 343 filterCategory[categoryName] += f"<tr valign='top'><td>{event['id'].split(baseid)[-1]}</td>" + \ 344 f"<td>{event.get('name', '')}</td>" + \ 345 f"<td>{event.get('valueStateId', '').split(baseid)[-1]}</td>" + \ 346 f"<td>{event.get('format', '')}</td>" + \ 347 f"<td>{event.get('valueType', '')}</td>" + \ 348 "<td>" 349 350 if len(event.get('valueChoices', [])) > 5: 351 filterCategory[categoryName] += f"<details><summary><ins>detail</ins></summary>\n" 352 needDropdown = True 353 354 filterCategory[categoryName] += f"<ul>" 355 for item in event.get('valueChoices', []): 356 filterCategory[categoryName] += f"<li>{item}</li>" 357 filterCategory[categoryName] += "</ul></td>" 358 359 if needDropdown: 360 filterCategory[categoryName] += "</details>" 361 eventDoc += "<td></tr>\n" 362 363 for category in filterCategory: 364 eventDoc += filterCategory[category] 365 eventDoc += f"</table></details>\n" 366 eventDoc += "<br>\n" 367 368 return eventDoc
def
main(docArg=None)
370def main(docArg=None): 371 parser = ArgumentParser(description= 372 "Script to automatically generates a documentation for a TouchPortal plugin.") 373 374 parser.add_argument( 375 "target", metavar='<target>', type=str, 376 help='tppdoc is a documanentation generator for TouchPortal plugins. It uses py entry to generates a markdown file.' + \ 377 'It can generate a table for the plugin settings, connectors, actions, state, and event. and It will also show' + \ 378 ' data field example, It can show max length, min value, max value for the data field.' 379 ) 380 381 parser.add_argument( 382 "-i", "--ignoreError", action='store_true', default=False, 383 help='Ignore error when validating. Default is False.' 384 ) 385 386 parser.add_argument( 387 "-o", "--output", default="Documentation.md", 388 help='Name of generated documentation. Default is "Documentation". You do not need to add the extension.' 389 ) 390 391 opts = parser.parse_args(docArg) 392 del parser 393 394 out_dir = os.path.dirname(opts.target) 395 targetPathbaseName = os.path.basename(opts.target) 396 397 if out_dir: 398 os.chdir(out_dir) 399 400 entryType = "py" if targetPathbaseName.endswith(".py") else "tp" 401 if not opts.ignoreError: 402 print("vaildating entry...\n") 403 if entryType == "tp" and _validateDefinition(targetPathbaseName): 404 print(targetPathbaseName, "is vaild file. continue building document.\n") 405 elif entryType == "py" and _validateDefinition(generateDefinitionFromScript(targetPathbaseName), as_str=True): 406 print(targetPathbaseName, "is vaild file. continue building document.\n") 407 else: 408 print("File is invalid. Please above error for more information.") 409 return -1 410 else: 411 print("Ignoring errors, contiune building document.\n") 412 413 if entryType == "py": entry = getInfoFromBuildScript(targetPathbaseName) 414 else: entry = TpToPy.toString(targetPathbaseName) 415 416 417 documentation = """""" 418 419 print("Building table of content\n") 420 tableContent = generateTableContent(entry.TP_PLUGIN_INFO, entry) 421 documentation += tableContent 422 423 documentation += f""" 424# Description 425 426""" 427 if entry.TP_PLUGIN_INFO.get('doc') and entry.TP_PLUGIN_INFO['doc'].get('description'): 428 documentation += f"{entry.TP_PLUGIN_INFO['doc']['description']}\n\n" 429 430 documentation += f"This documentation generated for {entry.TP_PLUGIN_INFO['name']} V{entry.TP_PLUGIN_INFO['version']} with [Python TouchPortal SDK](https://github.com/KillerBOSS2019/TouchPortal-API)." 431 if entry.TP_PLUGIN_SETTINGS: 432 print("Generating settings section\n") 433 setting = generateSetting(entry.TP_PLUGIN_SETTINGS) 434 documentation += setting 435 436 documentation += "\n# Features\n" 437 categoryStruct = entry.TP_PLUGIN_CATEGORIES 438 439 if "TP_PLUGIN_ACTIONS" in dir(entry) and entry.TP_PLUGIN_ACTIONS: 440 print("Generating action section\n") 441 action = generateAction(entry.TP_PLUGIN_ACTIONS, categoryStruct) 442 documentation += action 443 444 445 if "TP_PLUGIN_CONNECTORS" in dir(entry) and entry.TP_PLUGIN_CONNECTORS: 446 print("Generating connector section\n") 447 connector = generateConnectors(entry.TP_PLUGIN_CONNECTORS, categoryStruct) 448 documentation += connector 449 450 if "TP_PLUGIN_STATES" in dir(entry) and entry.TP_PLUGIN_STATES: 451 print("Generating state section\n") 452 state = generateState(entry.TP_PLUGIN_STATES, entry.TP_PLUGIN_INFO['id'], categoryStruct) 453 documentation += state 454 455 if "TP_PLUGIN_EVENTS" in dir(entry) and entry.TP_PLUGIN_EVENTS: 456 print("Generating event section\n") 457 event = generateEvent(entry.TP_PLUGIN_EVENTS, entry.TP_PLUGIN_INFO['id'], categoryStruct) 458 documentation += event 459 460 if entry.TP_PLUGIN_INFO.get("doc") and entry.TP_PLUGIN_INFO['doc'].get('Install'): 461 print("Found install method. Generating install section\n") 462 documentation += "\n# Installation\n" 463 documentation += entry.TP_PLUGIN_INFO['doc']['Install'] 464 465 print("Generating Bugs and Suggestion section\n") 466 documentation += "\n# Bugs and Suggestion\n" 467 try: 468 documentation += f"Open an [issue](https://github.com/{'/'.join(entry.TP_PLUGIN_INFO['doc']['repository'].split(':'))}/issues) or join offical [TouchPortal Discord](https://discord.gg/MgxQb8r) for support.\n\n" 469 except: 470 documentation += f"Open an issue on github or join offical [TouchPortal Discord](https://discord.gg/MgxQb8r) for support.\n\n" 471 472 documentation += "\n# License\n" 473 documentation += "This plugin is licensed under the [GPL 3.0 License] - see the [LICENSE](LICENSE) file for more information.\n\n" 474 475 with open(opts.output, "w") as f: 476 f.write(documentation) 477 478 print("Finished generating documentation.") 479 return 0