#!/usr/bin/env python3
# This file is part of the LibreOffice project.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Script to generate https://wiki.documentfoundation.org/Development/DispatchCommands
3 types of source files are scanned to identify and describe a list of relevant UNO commands:
- .hxx files: containing the symbolic and numeric id's, and the respective modes and groups
- .xcu files; containing several english labels as they appear in menus or tooltips
- .sdi files: containing a list of potential arguments for the commands, and their types
"""
import os
import sys
# It is assumed that the script is called from $BUILDDIR;
# and __file__ refers to the script location in $SRCDIR.
# This allows to use it in separate builddir configuration.
srcdir = os.path.dirname(os.path.realpath(__file__)) + '/..' # go up from /bin
builddir = os.getcwd()
REPO = 'https://opengrok.libreoffice.org/xref/core'
BLACKLIST = ('_SwitchViewShell0', '_SwitchViewShell1', '_SwitchViewShell2', '_SwitchViewShell3', '_SwitchViewShell4')
XCU_DIR = srcdir + '/officecfg/registry/data/org/openoffice/Office/UI/'
XCU_FILES = ( XCU_DIR + 'BasicIDECommands.xcu',
XCU_DIR + 'CalcCommands.xcu',
XCU_DIR + 'ChartCommands.xcu',
XCU_DIR + 'DbuCommands.xcu',
XCU_DIR + 'DrawImpressCommands.xcu',
XCU_DIR + 'GenericCommands.xcu',
XCU_DIR + 'MathCommands.xcu',
XCU_DIR + 'ReportCommands.xcu',
XCU_DIR + 'WriterCommands.xcu')
HXX_DIR = builddir + '/workdir/SdiTarget/'
HXX_FILES = ( HXX_DIR + 'basctl/sdi/basslots.hxx',
HXX_DIR + 'sc/sdi/scslots.hxx',
HXX_DIR + 'sd/sdi/sdgslots.hxx',
HXX_DIR + 'sd/sdi/sdslots.hxx',
HXX_DIR + 'sfx2/sdi/sfxslots.hxx',
HXX_DIR + 'starmath/sdi/smslots.hxx',
HXX_DIR + 'svx/sdi/svxslots.hxx',
HXX_DIR + 'sw/sdi/swslots.hxx')
SDI_FILES = ( srcdir + '/sc/sdi/scalc.sdi',
srcdir + '/sd/sdi/sdraw.sdi',
srcdir + '/sfx2/sdi/sfx.sdi',
srcdir + '/starmath/sdi/smath.sdi',
srcdir + '/svx/sdi/svx.sdi',
srcdir + '/sw/sdi/swriter.sdi')
# Category is defined by the 1st file where the command has been found. Precedence: 1. xcu, 2. hxx, 3. sdi.
MODULES = {'BasicIDE': 'Basic IDE, Forms, Dialogs',
'Calc': 'Calc',
'Chart': 'Charts',
'Dbu': 'Base',
'DrawImpress': 'Draw / Impress',
'Generic': 'Global',
'Math': 'Math',
'Report': 'Reports',
'Writer': 'Writer',
'basslots': 'Basic IDE, Forms, Dialogs',
'scslots': 'Calc',
'sdgslots': 'Draw / Impress',
'sdslots': 'Draw / Impress',
'sfxslots': 'Global',
'smslots': 'Math',
'svxslots': 'Global',
'swslots': 'Writer',
'scalc': 'Calc',
'sdraw': 'Draw / Impress',
'sfx': 'Global',
'smath': 'Math',
'svx': 'Global',
'swriter': 'Writer'}
def newcommand(unocommand):
cmd = {'unocommand': unocommand,
'module': '',
'xcufile': -1,
'xculinenumber': 0,
'xcuoccurs': 0,
'label': '',
'contextlabel': '',
'tooltiplabel': '',
'hxxfile': -1,
'hxxoccurs': 0,
'hxxlinenumber': 0,
'resourceid': '',
'numericid': '',
'group': '',
'sdifile': -1,
'sdioccurs': 0,
'sdilinenumber': 0,
'mode': '',
'arguments': ''}
return cmd
def get_uno(cmd):
if cmd.startswith('FocusToFindbar'):
cmd = 'vnd.sun.star.findbar:' + cmd
else:
cmd = '.uno:' + cmd
return cmd
def analyze_xcu(all_commands):
for filename in XCU_FILES:
ln = 0
with open(filename) as fh:
popups = False
for line in fh:
ln += 1
if '' in line:
popups = True
continue
elif popups is True and line == ' ':
popups = False
continue
if '' not in line:
try:
line = next(fh)
ln += 1
except StopIteration:
print("Warning: couldn't find '' line in %s" % filename,
file=sys.stderr)
break
if '' in line:
labeltext = line.replace('', '').replace('', '').strip()
if popups is False:
if command_name not in all_commands:
all_commands[command_name] = newcommand(command_name)
#
all_commands[command_name]['xcufile'] = XCU_FILES.index(filename)
all_commands[command_name]['xculinenumber'] = cmdln
all_commands[command_name][label] = labeltext.replace('~', '')
all_commands[command_name]['xcuoccurs'] += 1
def analyze_hxx(all_commands):
for filename in HXX_FILES:
with open(filename) as fh:
ln = 0
mode = ''
for line in fh:
ln += 1
if not line.startswith('// Slot Nr. '):
continue
# Parse sth like
# // Slot Nr. 0 : 5502
# SFX_NEW_SLOT_ARG( basctl_Shell,SID_SAVEASDOC,SfxGroupId::Document,
cmdln = ln
tmp = line.split(':')
command_id = tmp[1].strip()
line = next(fh)
ln += 1
tmp = line.split(',')
command_rid = tmp[1]
command_group = tmp[2].split('::')[1]
next(fh)
ln += 1
next(fh)
ln += 1
line = next(fh)
ln += 1
mode += 'U' if 'AUTOUPDATE' in line else ''
mode += 'M' if 'MENUCONFIG' in line else ''
mode += 'T' if 'TOOLBOXCONFIG' in line else ''
mode += 'A' if 'ACCELCONFIG' in line else ''
next(fh)
ln += 1
next(fh)
ln += 1
line = next(fh)
ln += 1
if '"' not in line:
line = next(fh)
tmp = line.split('"')
try:
command_name = get_uno(tmp[1])
except IndexError:
print("Warning: expected \" in line '%s' from file %s" % (line.strip(), filename),
file=sys.stderr)
command_name = '.uno:'
if command_name not in all_commands:
all_commands[command_name] = newcommand(command_name)
#
all_commands[command_name]['hxxfile'] = HXX_FILES.index(filename)
all_commands[command_name]['hxxlinenumber'] = cmdln
all_commands[command_name]['numericid'] = command_id
all_commands[command_name]['resourceid'] = command_rid
all_commands[command_name]['group'] = command_group
all_commands[command_name]['mode'] = mode
all_commands[command_name]['hxxoccurs'] += 1
mode = ''
def analyze_sdi(all_commands):
def SplitArguments(params):
# Split a string like : SfxStringItem Name SID_CHART_NAME,SfxStringItem Range SID_CHART_SOURCE,SfxBoolItem ColHeaders FN_PARAM_1,SfxBoolItem RowHeaders FN_PARAM_2
# in : Name (string)\nRange (string)\nRowHeaders (bool)
CR = '
'
split = ''
params = params.strip(' ,').replace(', ', ',') # At least 1 case of ', ' in svx/sdi/svx.sdi line 3592
if len(params) > 0:
for p in params.split(','):
if len(split) > 0:
split += CR
elems = p.split()
if len(elems) >= 2:
split += elems[1]
if 'String' in elems[0]:
split += ' (string)'
elif 'Bool' in elems[0]:
split += ' (bool)'
elif 'Int16' in elems[0]:
split += ' (integer)'
elif 'Int32' in elems[0]:
split += ' (long)'
else:
split += ' (' + elems[0].replace('Sfx', '').replace('Svx', '').replace('Item', '').lower() + ')'
return split
for filename in SDI_FILES:
ln = 0
comment, square, command, param = False, False, False, False
with open(filename) as fh:
for line in fh:
ln += 1
line = line.replace(' ', ' ').strip() # Anomaly met in svx/sdi/svx.sdi
if line.startswith('//'):
pass
elif comment is False and line.startswith('/*') and not line.endswith('*/'):
comment = True
elif comment is True and line.endswith('*/'):
comment = False
elif comment is False and line.startswith('/*') and line.endswith('*/'):
pass
elif comment is True:
pass
elif square is False and line.startswith('['):
square = True
mode = ''
command = False
elif square is True and line.endswith(']'):
all_commands[command_name]['mode'] = mode
square = False
elif square is True:
squaremode = line.strip(',;').split()
if len(squaremode) == 3:
mode += 'U' if squaremode[0] == 'AutoUpdate' and squaremode[2] == 'TRUE' else ''
mode += 'M' if squaremode[0] == 'MenuConfig' and squaremode[2] == 'TRUE' else ''
mode += 'T' if squaremode[0] == 'ToolBoxConfig' and squaremode[2] == 'TRUE' else ''
mode += 'A' if squaremode[0] == 'AccelConfig' and squaremode[2] == 'TRUE' else ''
elif comment is False and square is False and command is False and len(line) == 0:
pass
elif command is False:
command_name = get_uno(line.split(' ')[1])
if command_name not in all_commands:
all_commands[command_name] = newcommand(command_name)
all_commands[command_name]['sdifile'] = SDI_FILES.index(filename)
all_commands[command_name]['sdilinenumber'] = ln
all_commands[command_name]['sdioccurs'] += 1
if len(all_commands[command_name]['resourceid']) == 0:
all_commands[command_name]['resourceid'] = line.split(' ')[2]
command = True
elif command is True and (line == '' or line == '()'):
command = False
elif command is True and (param is True or line.startswith('(')) and line.endswith(')'):
if param:
params += line.strip(' (),').replace(', ', ',') # At least 1 case of ", " in svx/sdi/svx.sdi line 8767
# At least 1 case of "( " in sw/sdi/swriter.sdi line 5477
else:
params = line.strip(' (),').replace(', ', ',') # At least 1 case in sw/sdi/swriter.sdi line 7083
all_commands[command_name]['arguments'] = SplitArguments(params)
command = False
param = False
elif command is True and line.startswith('('): # Arguments always on 1 line, except in some cases (cfr.BasicIDEAppear)
params = line.strip(' ()').replace(', ', ',')
param = True
elif param is True:
params += line
def categorize(all_commands):
# Clean black listed commands
for command in BLACKLIST:
cmd = get_uno(command)
if cmd in all_commands:
del all_commands[cmd]
# Set category based on the file name where the command was found first
for cmd in all_commands:
command = all_commands[cmd]
cxcu, chxx, csdi = '', '', ''
fxcu = command['xcufile']
if fxcu > -1:
cxcu = os.path.basename(XCU_FILES[fxcu]).split('.')[0].replace('Commands', '')
fhxx = command['hxxfile']
if fhxx > -1:
chxx = os.path.basename(HXX_FILES[fhxx]).split('.')[0]
fsdi = command['sdifile']
if fsdi > -1:
csdi = os.path.basename(SDI_FILES[fsdi]).split('.')[0]
# General rule:
if len(cxcu) > 0:
cat = cxcu
elif len(chxx) > 0:
cat = chxx
else:
cat = csdi
# Exceptions on general rule
if cat == 'Generic' and chxx == 'basslots':
cat = chxx
command['module'] = MODULES[cat]
def print_output(all_commands):
def longest(*args):
# Return the longest string among the arguments
return max(args, key = len)
#
def sources(cmd):
# Build string identifying the sources
xcufile, xculinenumber, hxxfile, hxxlinenumber, sdifile, sdilinenumber = 2, 3, 8, 10, 14, 16
src = ''
if cmd[xcufile] >= 0:
src += '[' + REPO + XCU_FILES[cmd[xcufile]].replace(srcdir, '') + '#' + str(cmd[xculinenumber]) + ' XCU]'
if cmd[sdifile] >= 0:
src += ' [' + REPO + SDI_FILES[cmd[sdifile]].replace(srcdir, '') + '#' + str(cmd[sdilinenumber]) + ' SDI]'
if cmd[hxxfile] >= 0:
file = str(cmd[hxxfile] + 1 + len(XCU_FILES) + len(SDI_FILES))
src += ' [[#hxx' + file + '|HXX]]'
return src.strip()
#
# Sort by category and command name
commands_list = []
for cmd in all_commands:
cmdlist = tuple(all_commands[cmd].values())
commands_list.append(cmdlist)
sorted_by_command = sorted(commands_list, key = lambda cmd: cmd[0])
sorted_by_module = sorted(sorted_by_command, key = lambda cmd: cmd[1])
#
# Produce tabular output
unocommand, module, label, contextlabel, tooltiplabel, arguments, resourceid, numericid, group, mode = 0, 1, 5, 6, 7, 18, 11, 12, 13, 17
lastmodule = ''
for cmd in sorted_by_module:
# Format bottom and header
if lastmodule != cmd[module]:
if len(lastmodule) > 0:
print('\n|-\n|}\n')
print('')
lastmodule = cmd[module]
print('=== %s ===\n' % lastmodule)
print('')
print('{| class="wikitable sortable" width="100%"')
print('|-')
print('! scope="col" | Dispatch command')
print('! scope="col" | Description')
print('! scope="col" | Group')
print('! scope="col" | Arguments')
print('! scope="col" | Internal
name (value)')
print('! scope="col" | Mode')
print('! scope="col" | Source
files')
print('|-\n')
print('| ' + cmd[unocommand].replace('&', '\n&'))
print('| ' + longest(cmd[label], cmd[contextlabel], cmd[tooltiplabel]))
print('| ' + cmd[group])
print('| ' + cmd[arguments].replace('\\n', '\n'))
if len(cmd[numericid]) == 0:
print('| ' + cmd[resourceid])
else:
print('| ' + cmd[resourceid] + ' (' + cmd[numericid] + ')')
print('| ' + cmd[mode])
print('| ' + sources(cmd))
print('|-\n|}\n')
# List the source files
print('== Source files ==\n')
fn = 0
for i in range(len(XCU_FILES)):
fn += 1
print(f'({fn}) {REPO}{XCU_FILES[i]}\n'.replace(srcdir, ''))
print('\n')
for i in range(len(SDI_FILES)):
fn += 1
print(f'({fn}) {REPO}{SDI_FILES[i]}\n'.replace(srcdir, ''))
print('\n')
for i in range(len(HXX_FILES)):
fn += 1
print(f'({fn}) {HXX_FILES[i]}\n'.replace(builddir, ''))
print('')
def main():
all_commands = {}
analyze_xcu(all_commands)
analyze_hxx(all_commands)
analyze_sdi(all_commands)
categorize(all_commands)
print_output(all_commands)
if __name__ == '__main__':
main()