123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721 |
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- import codecs
- import sys
- import os
- import re
- import xml.etree.ElementTree as ET
- input_list = []
- cur_file = ""
- # http(s)://docs.godotengine.org/<langcode>/<tag>/path/to/page.html(#fragment-tag)
- godot_docs_pattern = re.compile('^http(?:s)?:\/\/docs\.godotengine\.org\/(?:[a-zA-Z0-9\.\-_]*)\/(?:[a-zA-Z0-9\.\-_]*)\/(.*)\.html(#.*)?$')
- for arg in sys.argv[1:]:
- if arg.endswith(os.sep):
- arg = arg[:-1]
- input_list.append(arg)
- if len(input_list) < 1:
- print('usage: makerst.py <path to folders> and/or <path to .xml files> (order of arguments irrelevant)')
- print('example: makerst.py "../../modules/" "../classes" path_to/some_class.xml')
- sys.exit(0)
- def validate_tag(elem, tag):
- if elem.tag != tag:
- print("Tag mismatch, expected '" + tag + "', got " + elem.tag)
- sys.exit(255)
- class_names = []
- classes = {}
- def ul_string(str, ul):
- str += "\n"
- for i in range(len(str) - 1):
- str += ul
- str += "\n"
- return str
- def make_class_list(class_list, columns):
- f = codecs.open('class_list.rst', 'wb', 'utf-8')
- prev = 0
- col_max = len(class_list) / columns + 1
- print(('col max is ', col_max))
- col_count = 0
- row_count = 0
- last_initial = ''
- fit_columns = []
- for n in range(0, columns):
- fit_columns += [[]]
- indexers = []
- last_initial = ''
- idx = 0
- for n in class_list:
- col = idx / col_max
- if col >= columns:
- col = columns - 1
- fit_columns[col] += [n]
- idx += 1
- if n[:1] != last_initial:
- indexers += [n]
- last_initial = n[:1]
- row_max = 0
- f.write("\n")
- for n in range(0, columns):
- if len(fit_columns[n]) > row_max:
- row_max = len(fit_columns[n])
- f.write("| ")
- for n in range(0, columns):
- f.write(" | |")
- f.write("\n")
- f.write("+")
- for n in range(0, columns):
- f.write("--+-------+")
- f.write("\n")
- for r in range(0, row_max):
- s = '+ '
- for c in range(0, columns):
- if r >= len(fit_columns[c]):
- continue
- classname = fit_columns[c][r]
- initial = classname[0]
- if classname in indexers:
- s += '**' + initial + '** | '
- else:
- s += ' | '
- s += '[' + classname + '](class_' + classname.lower() + ') | '
- s += '\n'
- f.write(s)
- for n in range(0, columns):
- f.write("--+-------+")
- f.write("\n")
- f.close()
- def rstize_text(text, cclass):
- # Linebreak + tabs in the XML should become two line breaks unless in a "codeblock"
- pos = 0
- while True:
- pos = text.find('\n', pos)
- if pos == -1:
- break
- pre_text = text[:pos]
- while text[pos + 1] == '\t':
- pos += 1
- post_text = text[pos + 1:]
- # Handle codeblocks
- if post_text.startswith("[codeblock]"):
- end_pos = post_text.find("[/codeblock]")
- if end_pos == -1:
- sys.exit("ERROR! [codeblock] without a closing tag!")
- code_text = post_text[len("[codeblock]"):end_pos]
- post_text = post_text[end_pos:]
- # Remove extraneous tabs
- code_pos = 0
- while True:
- code_pos = code_text.find('\n', code_pos)
- if code_pos == -1:
- break
- to_skip = 0
- while code_pos + to_skip + 1 < len(code_text) and code_text[code_pos + to_skip + 1] == '\t':
- to_skip += 1
- if len(code_text[code_pos + to_skip + 1:]) == 0:
- code_text = code_text[:code_pos] + "\n"
- code_pos += 1
- else:
- code_text = code_text[:code_pos] + "\n " + code_text[code_pos + to_skip + 1:]
- code_pos += 5 - to_skip
- text = pre_text + "\n[codeblock]" + code_text + post_text
- pos += len("\n[codeblock]" + code_text)
- # Handle normal text
- else:
- text = pre_text + "\n\n" + post_text
- pos += 2
- next_brac_pos = text.find('[')
- # Escape \ character, otherwise it ends up as an escape character in rst
- pos = 0
- while True:
- pos = text.find('\\', pos, next_brac_pos)
- if pos == -1:
- break
- text = text[:pos] + "\\\\" + text[pos + 1:]
- pos += 2
- # Escape * character to avoid interpreting it as emphasis
- pos = 0
- while True:
- pos = text.find('*', pos, next_brac_pos)
- if pos == -1:
- break
- text = text[:pos] + "\*" + text[pos + 1:]
- pos += 2
- # Escape _ character at the end of a word to avoid interpreting it as an inline hyperlink
- pos = 0
- while True:
- pos = text.find('_', pos, next_brac_pos)
- if pos == -1:
- break
- if not text[pos + 1].isalnum(): # don't escape within a snake_case word
- text = text[:pos] + "\_" + text[pos + 1:]
- pos += 2
- else:
- pos += 1
- # Handle [tags]
- inside_code = False
- pos = 0
- while True:
- pos = text.find('[', pos)
- if pos == -1:
- break
- endq_pos = text.find(']', pos + 1)
- if endq_pos == -1:
- break
- pre_text = text[:pos]
- post_text = text[endq_pos + 1:]
- tag_text = text[pos + 1:endq_pos]
- escape_post = False
- if tag_text in class_names:
- tag_text = make_type(tag_text)
- escape_post = True
- else: # command
- cmd = tag_text
- space_pos = tag_text.find(' ')
- if cmd == '/codeblock':
- tag_text = ''
- inside_code = False
- # Strip newline if the tag was alone on one
- if pre_text[-1] == '\n':
- pre_text = pre_text[:-1]
- elif cmd == '/code':
- tag_text = '``'
- inside_code = False
- escape_post = True
- elif inside_code:
- tag_text = '[' + tag_text + ']'
- elif cmd.find('html') == 0:
- cmd = tag_text[:space_pos]
- param = tag_text[space_pos + 1:]
- tag_text = param
- elif cmd.find('method') == 0 or cmd.find('member') == 0 or cmd.find('signal') == 0:
- cmd = tag_text[:space_pos]
- param = tag_text[space_pos + 1:]
- if param.find('.') != -1:
- ss = param.split('.')
- if len(ss) > 2:
- sys.exit("Bad reference: '" + param + "' in file: " + cur_file)
- (class_param, method_param) = ss
- tag_text = ':ref:`' + class_param + '.' + method_param + '<class_' + class_param + '_' + method_param + '>`'
- else:
- tag_text = ':ref:`' + param + '<class_' + cclass + "_" + param + '>`'
- escape_post = True
- elif cmd.find('image=') == 0:
- tag_text = "" # ''
- elif cmd.find('url=') == 0:
- tag_text = ':ref:`' + cmd[4:] + '<' + cmd[4:] + ">`"
- elif cmd == '/url':
- tag_text = ''
- escape_post = True
- elif cmd == 'center':
- tag_text = ''
- elif cmd == '/center':
- tag_text = ''
- elif cmd == 'codeblock':
- tag_text = '\n::\n'
- inside_code = True
- elif cmd == 'br':
- # Make a new paragraph instead of a linebreak, rst is not so linebreak friendly
- tag_text = '\n\n'
- # Strip potential leading spaces
- while post_text[0] == ' ':
- post_text = post_text[1:]
- elif cmd == 'i' or cmd == '/i':
- tag_text = '*'
- elif cmd == 'b' or cmd == '/b':
- tag_text = '**'
- elif cmd == 'u' or cmd == '/u':
- tag_text = ''
- elif cmd == 'code':
- tag_text = '``'
- inside_code = True
- elif cmd.startswith('enum '):
- tag_text = make_enum(cmd[5:])
- else:
- tag_text = make_type(tag_text)
- escape_post = True
- # Properly escape things like `[Node]s`
- if escape_post and post_text and post_text[0].isalnum(): # not punctuation, escape
- post_text = '\ ' + post_text
- next_brac_pos = post_text.find('[', 0)
- iter_pos = 0
- while not inside_code:
- iter_pos = post_text.find('*', iter_pos, next_brac_pos)
- if iter_pos == -1:
- break
- post_text = post_text[:iter_pos] + "\*" + post_text[iter_pos + 1:]
- iter_pos += 2
- iter_pos = 0
- while not inside_code:
- iter_pos = post_text.find('_', iter_pos, next_brac_pos)
- if iter_pos == -1:
- break
- if not post_text[iter_pos + 1].isalnum(): # don't escape within a snake_case word
- post_text = post_text[:iter_pos] + "\_" + post_text[iter_pos + 1:]
- iter_pos += 2
- else:
- iter_pos += 1
- text = pre_text + tag_text + post_text
- pos = len(pre_text) + len(tag_text)
- return text
- def format_table(f, pp):
- longest_t = 0
- longest_s = 0
- for s in pp:
- sl = len(s[0])
- if (sl > longest_s):
- longest_s = sl
- tl = len(s[1])
- if (tl > longest_t):
- longest_t = tl
- sep = "+"
- for i in range(longest_s + 2):
- sep += "-"
- sep += "+"
- for i in range(longest_t + 2):
- sep += "-"
- sep += "+\n"
- f.write(sep)
- for s in pp:
- rt = s[0]
- while (len(rt) < longest_s):
- rt += " "
- st = s[1]
- while (len(st) < longest_t):
- st += " "
- f.write("| " + rt + " | " + st + " |\n")
- f.write(sep)
- f.write('\n')
- def make_type(t):
- global class_names
- if t in class_names:
- return ':ref:`' + t + '<class_' + t + '>`'
- return t
- def make_enum(t):
- global class_names
- p = t.find(".")
- # Global enums such as Error are relative to @GlobalScope.
- if p >= 0:
- c = t[0:p]
- e = t[p + 1:]
- # Variant enums live in GlobalScope but still use periods.
- if c == "Variant":
- c = "@GlobalScope"
- e = "Variant." + e
- else:
- # Things in GlobalScope don't have a period.
- c = "@GlobalScope"
- e = t
- if c in class_names:
- return ':ref:`' + e + '<enum_' + c + '_' + e + '>`'
- return t
- def make_method(
- f,
- cname,
- method_data,
- declare,
- event=False,
- pp=None
- ):
- if (declare or pp == None):
- t = '- '
- else:
- t = ""
- ret_type = 'void'
- args = list(method_data)
- mdata = {}
- mdata['argidx'] = []
- for a in args:
- if a.tag == 'return':
- idx = -1
- elif a.tag == 'argument':
- idx = int(a.attrib['index'])
- else:
- continue
- mdata['argidx'].append(idx)
- mdata[idx] = a
- if not event:
- if -1 in mdata['argidx']:
- if 'enum' in mdata[-1].attrib:
- t += make_enum(mdata[-1].attrib['enum'])
- else:
- t += make_type(mdata[-1].attrib['type'])
- else:
- t += 'void'
- t += ' '
- if declare or pp == None:
- s = '**' + method_data.attrib['name'] + '** '
- else:
- s = ':ref:`' + method_data.attrib['name'] + '<class_' + cname + "_" + method_data.attrib['name'] + '>` '
- s += '**(**'
- argfound = False
- for a in mdata['argidx']:
- arg = mdata[a]
- if a < 0:
- continue
- if a > 0:
- s += ', '
- else:
- s += ' '
- if 'enum' in arg.attrib:
- s += make_enum(arg.attrib['enum'])
- else:
- s += make_type(arg.attrib['type'])
- if 'name' in arg.attrib:
- s += ' ' + arg.attrib['name']
- else:
- s += ' arg' + str(a)
- if 'default' in arg.attrib:
- s += '=' + arg.attrib['default']
- s += ' **)**'
- if 'qualifiers' in method_data.attrib:
- s += ' ' + method_data.attrib['qualifiers']
- if (not declare):
- if (pp != None):
- pp.append((t, s))
- else:
- f.write("- " + t + " " + s + "\n")
- else:
- f.write(t + s + "\n")
- def make_properties(
- f,
- cname,
- prop_data,
- description=False,
- pp=None
- ):
- t = ""
- if 'enum' in prop_data.attrib:
- t += make_enum(prop_data.attrib['enum'])
- else:
- t += make_type(prop_data.attrib['type'])
- if description:
- s = '**' + prop_data.attrib['name'] + '**'
- setget = []
- if 'setter' in prop_data.attrib and prop_data.attrib['setter'] != '' and not prop_data.attrib['setter'].startswith('_'):
- setget.append(("*Setter*", prop_data.attrib['setter'] + '(value)'))
- if 'getter' in prop_data.attrib and prop_data.attrib['getter'] != '' and not prop_data.attrib['getter'].startswith('_'):
- setget.append(('*Getter*', prop_data.attrib['getter'] + '()'))
- else:
- s = ':ref:`' + prop_data.attrib['name'] + '<class_' + cname + "_" + prop_data.attrib['name'] + '>`'
- if (pp != None):
- pp.append((t, s))
- elif description:
- f.write('- ' + t + ' ' + s + '\n\n')
- if len(setget) > 0:
- format_table(f, setget)
- def make_heading(title, underline):
- return title + '\n' + underline * len(title) + "\n\n"
- def make_rst_class(node):
- name = node.attrib['name']
- f = codecs.open("class_" + name.lower() + '.rst', 'wb', 'utf-8')
- # Warn contributors not to edit this file directly
- f.write(".. Generated automatically by doc/tools/makerst.py in Godot's source tree.\n")
- f.write(".. DO NOT EDIT THIS FILE, but the " + name + ".xml source instead.\n")
- f.write(".. The source is found in doc/classes or modules/<name>/doc_classes.\n\n")
- f.write(".. _class_" + name + ":\n\n")
- f.write(make_heading(name, '='))
- # Inheritance tree
- # Ascendents
- if 'inherits' in node.attrib:
- inh = node.attrib['inherits'].strip()
- f.write('**Inherits:** ')
- first = True
- while (inh in classes):
- if (not first):
- f.write(" **<** ")
- else:
- first = False
- f.write(make_type(inh))
- inode = classes[inh]
- if ('inherits' in inode.attrib):
- inh = inode.attrib['inherits'].strip()
- else:
- inh = None
- f.write("\n\n")
- # Descendents
- inherited = []
- for cn in classes:
- c = classes[cn]
- if 'inherits' in c.attrib:
- if (c.attrib['inherits'].strip() == name):
- inherited.append(c.attrib['name'])
- if (len(inherited)):
- f.write('**Inherited By:** ')
- for i in range(len(inherited)):
- if (i > 0):
- f.write(", ")
- f.write(make_type(inherited[i]))
- f.write("\n\n")
- # Category
- if 'category' in node.attrib:
- f.write('**Category:** ' + node.attrib['category'].strip() + "\n\n")
- # Brief description
- f.write(make_heading('Brief Description', '-'))
- briefd = node.find('brief_description')
- if briefd != None:
- f.write(rstize_text(briefd.text.strip(), name) + "\n\n")
- # Properties overview
- members = node.find('members')
- if members != None and len(list(members)) > 0:
- f.write(make_heading('Properties', '-'))
- ml = []
- for m in list(members):
- make_properties(f, name, m, False, ml)
- format_table(f, ml)
- # Methods overview
- methods = node.find('methods')
- if methods != None and len(list(methods)) > 0:
- f.write(make_heading('Methods', '-'))
- ml = []
- for m in list(methods):
- make_method(f, name, m, False, False, ml)
- format_table(f, ml)
- # Theme properties
- theme_items = node.find('theme_items')
- if theme_items != None and len(list(theme_items)) > 0:
- f.write(make_heading('Theme Properties', '-'))
- ml = []
- for m in list(theme_items):
- make_properties(f, name, m, False, ml)
- format_table(f, ml)
- # Signals
- events = node.find('signals')
- if events != None and len(list(events)) > 0:
- f.write(make_heading('Signals', '-'))
- for m in list(events):
- f.write(" .. _class_" + name + "_" + m.attrib['name'] + ":\n\n")
- make_method(f, name, m, True, True)
- f.write('\n')
- d = m.find('description')
- if d == None or d.text.strip() == '':
- continue
- f.write(rstize_text(d.text.strip(), name))
- f.write("\n\n")
- # Constants and enums
- constants = node.find('constants')
- consts = []
- enum_names = set()
- enums = []
- if constants != None and len(list(constants)) > 0:
- for c in list(constants):
- if 'enum' in c.attrib:
- enum_names.add(c.attrib['enum'])
- enums.append(c)
- else:
- consts.append(c)
- # Enums
- if len(enum_names) > 0:
- f.write(make_heading('Enumerations', '-'))
- for e in enum_names:
- f.write(" .. _enum_" + name + "_" + e + ":\n\n")
- f.write("enum **" + e + "**:\n\n")
- for c in enums:
- if c.attrib['enum'] != e:
- continue
- s = '- '
- s += '**' + c.attrib['name'] + '**'
- if 'value' in c.attrib:
- s += ' = **' + c.attrib['value'] + '**'
- if c.text.strip() != '':
- s += ' --- ' + rstize_text(c.text.strip(), name)
- f.write(s + '\n')
- f.write('\n')
- # Constants
- if len(consts) > 0:
- f.write(make_heading('Constants', '-'))
- for c in list(consts):
- s = '- '
- s += '**' + c.attrib['name'] + '**'
- if 'value' in c.attrib:
- s += ' = **' + c.attrib['value'] + '**'
- if c.text.strip() != '':
- s += ' --- ' + rstize_text(c.text.strip(), name)
- f.write(s + '\n')
- # Class description
- descr = node.find('description')
- if descr != None and descr.text.strip() != '':
- f.write(make_heading('Description', '-'))
- f.write(rstize_text(descr.text.strip(), name) + "\n\n")
- # Online tutorials
- global godot_docs_pattern
- tutorials = node.find('tutorials')
- if tutorials != None and len(tutorials) > 0:
- f.write(make_heading('Tutorials', '-'))
- for t in tutorials:
- link = t.text.strip()
- match = godot_docs_pattern.search(link);
- if match:
- groups = match.groups()
- if match.lastindex == 2:
- # Doc reference with fragment identifier: emit direct link to section with reference to page, for example:
- # `#calling-javascript-from-script in Exporting For Web`
- f.write("- `" + groups[1] + " <../" + groups[0] + ".html" + groups[1] + ">`_ in :doc:`../" + groups[0] + "`\n")
- # Commented out alternative: Instead just emit:
- # `Subsection in Exporting For Web`
- # f.write("- `Subsection <../" + groups[0] + ".html" + groups[1] + ">`_ in :doc:`../" + groups[0] + "`\n")
- elif match.lastindex == 1:
- # Doc reference, for example:
- # `Math`
- f.write("- :doc:`../" + groups[0] + "`\n")
- else:
- # External link, for example:
- # `http://enet.bespin.org/usergroup0.html`
- f.write("- `" + link + " <" + link + ">`_\n")
- # Property descriptions
- members = node.find('members')
- if members != None and len(list(members)) > 0:
- f.write(make_heading('Property Descriptions', '-'))
- for m in list(members):
- f.write(" .. _class_" + name + "_" + m.attrib['name'] + ":\n\n")
- make_properties(f, name, m, True)
- if m.text.strip() != '':
- f.write(rstize_text(m.text.strip(), name))
- f.write('\n\n')
- # Method descriptions
- methods = node.find('methods')
- if methods != None and len(list(methods)) > 0:
- f.write(make_heading('Method Descriptions', '-'))
- for m in list(methods):
- f.write(".. _class_" + name + "_" + m.attrib['name'] + ":\n\n")
- make_method(f, name, m, True)
- f.write('\n')
- d = m.find('description')
- if d == None or d.text.strip() == '':
- continue
- f.write(rstize_text(d.text.strip(), name))
- f.write("\n\n")
- file_list = []
- for path in input_list:
- if os.path.basename(path) == 'modules':
- for subdir, dirs, _ in os.walk(path):
- if 'doc_classes' in dirs:
- doc_dir = os.path.join(subdir, 'doc_classes')
- class_file_names = [f for f in os.listdir(doc_dir) if f.endswith('.xml')]
- file_list += [os.path.join(doc_dir, f) for f in class_file_names]
- elif not os.path.isfile(path):
- file_list += [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.xml')]
- elif os.path.isfile(path) and path.endswith('.xml'):
- file_list.append(path)
- for cur_file in file_list:
- tree = ET.parse(cur_file)
- doc = tree.getroot()
- if 'version' not in doc.attrib:
- print("Version missing from 'doc'")
- sys.exit(255)
- version = doc.attrib['version']
- if doc.attrib['name'] in class_names:
- continue
- class_names.append(doc.attrib['name'])
- classes[doc.attrib['name']] = doc
- class_names.sort()
- # Don't make class list for Sphinx, :toctree: handles it
- # make_class_list(class_names, 2)
- for cn in class_names:
- c = classes[cn]
- make_rst_class(c)
|