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": ""}) in TP_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" &nbsp; <b>Min Value:</b> {entry['minValue']}"
152    else:
153        typeDoc += " &nbsp; <b>Min Value:</b> -2147483648"
154
155    if entry.get('maxValue'):
156        typeDoc += f" &nbsp; <b>Max Value:</b> {entry['maxValue']}"
157    else:
158        typeDoc += f" &nbsp; <b>Max Value:</b> 2147483647"
159
160    if entry.get('allowDecimals'):
161        typeDoc += f" &nbsp; <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']} &nbsp; \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 += "&lt;empty&gt;"
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 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" &nbsp; <b>Min Value:</b> {entry['minValue']}"
153    else:
154        typeDoc += " &nbsp; <b>Min Value:</b> -2147483648"
155
156    if entry.get('maxValue'):
157        typeDoc += f" &nbsp; <b>Max Value:</b> {entry['maxValue']}"
158    else:
159        typeDoc += f" &nbsp; <b>Max Value:</b> 2147483647"
160
161    if entry.get('allowDecimals'):
162        typeDoc += f" &nbsp; <b>Allow Decimals:</b> {entry['allowDecimals']}"
163
164    return typeDoc
def getCategoryName(categoryId, categoryStruct)
196def getCategoryName(categoryId, categoryStruct):
197    try:
198        return categoryStruct[categoryId].get('name', categoryId)
199    except IndexError:
200        return categoryStruct[list(categoryStruct.keys())[0]] # If it does not have a `category` field It will use default
def getCategoryId(categoryId, categoryStruct)
202def getCategoryId(categoryId, categoryStruct):
203    return categoryStruct.get(categoryId)
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