can't appear inside
+ try:
+ if len(lines) == 1 and \
+ TARGET in ('html', 'xhtml') and \
+ re.match('^\s*
.*\s*$', lines[0]):
+ result = [lines[0]]
+ except: pass
+
+ return result
+
+ def verb(self):
+ "Verbatim lines are not masked, so there's no need to unmask"
+ result = []
+ open_ = TAGS['blockVerbOpen']
+ close = TAGS['blockVerbClose']
+
+ # Blank line before?
+ if self._should_add_blank_line('before', 'verb'): result.append('')
+
+ # Open tag
+ if open_: result.append(open_)
+
+ # Get contents
+ for line in self.hold():
+ if self.prop('mapped') == 'table':
+ line = MacroMaster().expand(line)
+ if not rules['verbblocknotescaped']:
+ line = doEscape(TARGET,line)
+ if rules['indentverbblock']:
+ line = ' '+line
+ if rules['verbblockfinalescape']:
+ line = doFinalEscape(TARGET, line)
+ result.append(line)
+
+ # Close tag
+ if close: result.append(close)
+
+ # Blank line after?
+ if self._should_add_blank_line('after', 'verb'): result.append('')
+
+ return result
+
+ def numtitle(self): return self.title('numtitle')
+ def title(self, name='title'):
+ result = []
+
+ # Blank line before?
+ if self._should_add_blank_line('before', name): result.append('')
+
+ # Get contents
+ result.extend(TITLE.get())
+
+ # Blank line after?
+ if self._should_add_blank_line('after', name): result.append('')
+
+ return result
+
+ def table(self):
+ result = []
+
+ # Blank line before?
+ if self._should_add_blank_line('before', 'table'): result.append('')
+
+ # Rewrite all table cells by the unmasked and escaped data
+ lines = self._get_escaped_hold()
+ for i in range(len(lines)):
+ cells = lines[i].split(SEPARATOR)
+ self.tableparser.rows[i]['cells'] = cells
+ result.extend(self.tableparser.dump())
+
+ # Blank line after?
+ if self._should_add_blank_line('after', 'table'): result.append('')
+
+ return result
+
+ def quote(self):
+ result = []
+ open_ = TAGS['blockQuoteOpen'] # block based
+ close = TAGS['blockQuoteClose']
+ qline = TAGS['blockQuoteLine'] # line based
+ indent = tagindent = '\t'*self.depth
+
+ # Apply rules
+ if rules['tagnotindentable']: tagindent = ''
+ if not rules['keepquoteindent']: indent = ''
+
+ # Blank line before?
+ if self._should_add_blank_line('before', 'quote'): result.append('')
+
+ # Open tag
+ if open_: result.append(tagindent+open_)
+
+ # Get contents
+ for item in self.hold():
+ if isinstance(item, type([])):
+ result.extend(item) # subquotes
+ else:
+ item = regex['quote'].sub('', item) # del TABs
+ item = self._last_escapes(item)
+ item = qline*self.depth + item
+ result.append(indent+item) # quote line
+
+ # Close tag
+ if close: result.append(tagindent+close)
+
+ # Blank line after?
+ if self._should_add_blank_line('after', 'quote'): result.append('')
+
+ return result
+
+ def bar(self):
+ result = []
+ bar_tag = ''
+
+ # Blank line before?
+ if self._should_add_blank_line('before', 'bar'): result.append('')
+
+ # Get the original bar chars
+ bar_chars = self.hold()[0].strip()
+
+ # Set bar type
+ if bar_chars.startswith('='): bar_tag = TAGS['bar2']
+ else : bar_tag = TAGS['bar1']
+
+ # To avoid comment tag confusion like (sgml)
+ if TAGS['comment'].count('--'):
+ bar_chars = bar_chars.replace('--', '__')
+
+ # Get the bar tag (may contain \a)
+ result.append(regex['x'].sub(bar_chars, bar_tag))
+
+ # Blank line after?
+ if self._should_add_blank_line('after', 'bar'): result.append('')
+
+ return result
+
+ def deflist(self): return self.list('deflist')
+ def numlist(self): return self.list('numlist')
+ def list(self, name='list'):
+ result = []
+ items = self.hold()
+ indent = self.prop('indent')
+ tagindent = indent
+ listline = TAGS.get(name+'ItemLine')
+ itemcount = 0
+
+ if name == 'deflist':
+ itemopen = TAGS[name+'Item1Open']
+ itemclose = TAGS[name+'Item2Close']
+ itemsep = TAGS[name+'Item1Close']+\
+ TAGS[name+'Item2Open']
+ else:
+ itemopen = TAGS[name+'ItemOpen']
+ itemclose = TAGS[name+'ItemClose']
+ itemsep = ''
+
+ # Apply rules
+ if rules['tagnotindentable']: tagindent = ''
+ if not rules['keeplistindent']: indent = tagindent = ''
+
+ # ItemLine: number of leading chars identifies list depth
+ if listline:
+ itemopen = listline*self.depth + itemopen
+
+ # Adds trailing space on opening tags
+ if (name == 'list' and rules['spacedlistitemopen']) or \
+ (name == 'numlist' and rules['spacednumlistitemopen']):
+ itemopen = itemopen + ' '
+
+ # Remove two-blanks from list ending mark, to avoid
+ items[-1] = self._remove_twoblanks(items[-1])
+
+ # Blank line before?
+ if self._should_add_blank_line('before', name): result.append('')
+
+ # Tag each list item (multiline items), store in listbody
+ itemopenorig = itemopen
+ listbody = []
+ widelist = 0
+ for item in items:
+
+ # Add "manual" item count for noautonum targets
+ itemcount += 1
+ if name == 'numlist' and not rules['autonumberlist']:
+ n = str(itemcount)
+ itemopen = regex['x'].sub(n, itemopenorig)
+ del n
+
+ # Tag it
+ item[0] = self._last_escapes(item[0])
+ if name == 'deflist':
+ z,term,rest = item[0].split(SEPARATOR, 2)
+ item[0] = rest
+ if not item[0]: del item[0] # to avoid
+ listbody.append(tagindent+itemopen+term+itemsep)
+ else:
+ fullitem = tagindent+itemopen
+ listbody.append(item[0].replace(SEPARATOR, fullitem))
+ del item[0]
+
+ # Process next lines for this item (if any)
+ for line in item:
+ if isinstance(line, type([])): # sublist inside
+ listbody.extend(line)
+ else:
+ line = self._last_escapes(line)
+
+ # Blank lines turns to
+ if not line and rules['parainsidelist']:
+ line = indent + TAGS['paragraphOpen'] + TAGS['paragraphClose']
+ line = line.rstrip()
+ widelist = 1
+
+ # Some targets don't like identation here (wiki)
+ if not rules['keeplistindent'] or (name == 'deflist' and rules['deflisttextstrip']):
+ line = line.lstrip()
+
+ # Maybe we have a line prefix to add? (wiki)
+ if name == 'deflist' and TAGS['deflistItem2LinePrefix']:
+ line = TAGS['deflistItem2LinePrefix'] + line
+
+ listbody.append(line)
+
+ # Close item (if needed)
+ if itemclose: listbody.append(tagindent+itemclose)
+
+ if not widelist and rules['compactlist']:
+ listopen = TAGS.get(name+'OpenCompact')
+ listclose = TAGS.get(name+'CloseCompact')
+ else:
+ listopen = TAGS.get(name+'Open')
+ listclose = TAGS.get(name+'Close')
+
+ # Open list (not nestable lists are only opened at mother)
+ if listopen and not \
+ (rules['listnotnested'] and BLOCK.depth != 1):
+ result.append(tagindent+listopen)
+
+ result.extend(listbody)
+
+ # Close list (not nestable lists are only closed at mother)
+ if listclose and not \
+ (rules['listnotnested'] and self.depth != 1):
+ result.append(tagindent+listclose)
+
+ # Blank line after?
+ if self._should_add_blank_line('after', name): result.append('')
+
+ return result
+
+
+##############################################################################
+
+
+class MacroMaster:
+ def __init__(self, config={}):
+ self.name = ''
+ self.config = config or CONF
+ self.infile = self.config['sourcefile']
+ self.outfile = self.config['outfile']
+ self.currdate = time.localtime(time.time())
+ self.rgx = regex.get('macros') or getRegexes()['macros']
+ self.fileinfo = { 'infile': None, 'outfile': None }
+ self.dft_fmt = MACROS
+
+ def walk_file_format(self, fmt):
+ "Walks the %%{in/out}file format string, expanding the % flags"
+ i = 0; ret = '' # counter/hold
+ while i < len(fmt): # char by char
+ c = fmt[i]; i += 1
+ if c == '%': # hot char!
+ if i == len(fmt): # % at the end
+ ret = ret + c
+ break
+ c = fmt[i]; i += 1 # read next
+ ret = ret + self.expand_file_flag(c)
+ else:
+ ret = ret +c # common char
+ return ret
+
+ def expand_file_flag(self, flag):
+ "%f: filename %F: filename (w/o extension)"
+ "%d: dirname %D: dirname (only parent dir)"
+ "%p: file path %e: extension"
+ info = self.fileinfo[self.name] # get dict
+ if flag == '%': x = '%' # %% -> %
+ elif flag == 'f': x = info['name']
+ elif flag == 'F': x = re.sub('\.[^.]*$','',info['name'])
+ elif flag == 'd': x = info['dir']
+ elif flag == 'D': x = os.path.split(info['dir'])[-1]
+ elif flag == 'p': x = info['path']
+ elif flag == 'e': x = re.search('.(\.([^.]+))?$', info['name']).group(2) or ''
+ #TODO simpler way for %e ?
+ else : x = '%'+flag # false alarm
+ return x
+
+ def set_file_info(self, macroname):
+ if self.fileinfo.get(macroname): return # already done
+ file_ = getattr(self, self.name) # self.infile
+ if file_ == STDOUT or file_ == MODULEOUT:
+ dir_ = ''
+ path = name = file_
+ else:
+ path = os.path.abspath(file_)
+ dir_ = os.path.dirname(path)
+ name = os.path.basename(path)
+ self.fileinfo[macroname] = {'path':path,'dir':dir_,'name':name}
+
+ def expand(self, line=''):
+ "Expand all macros found on the line"
+ while self.rgx.search(line):
+ m = self.rgx.search(line)
+ name = self.name = m.group('name').lower()
+ fmt = m.group('fmt') or self.dft_fmt.get(name)
+ if name == 'date':
+ txt = time.strftime(fmt,self.currdate)
+ elif name == 'mtime':
+ if self.infile in (STDIN, MODULEIN):
+ fdate = self.currdate
+ else:
+ mtime = os.path.getmtime(self.infile)
+ fdate = time.localtime(mtime)
+ txt = time.strftime(fmt,fdate)
+ elif name == 'infile' or name == 'outfile':
+ self.set_file_info(name)
+ txt = self.walk_file_format(fmt)
+ else:
+ Error("Unknown macro name '%s'"%name)
+ line = self.rgx.sub(txt,line,1)
+ return line
+
+
+##############################################################################
+
+
+def listTargets():
+ """list all available targets"""
+ targets = sorted(TARGETS)
+ for target in targets:
+ print("%s\t%s" % (target, TARGET_NAMES.get(target)))
+
+def dumpConfig(source_raw, parsed_config):
+ onoff = {1:_('ON'), 0:_('OFF')}
+ data = [
+ (_('RC file') , RC_RAW ),
+ (_('source document'), source_raw ),
+ (_('command line') , CMDLINE_RAW)
+ ]
+ # First show all RAW data found
+ for label, cfg in data:
+ print(_('RAW config for %s')%label)
+ for target,key,val in cfg:
+ target = '(%s)'%target
+ key = dotted_spaces("%-14s"%key)
+ val = val or _('ON')
+ print(' %-8s %s: %s'%(target,key,val))
+ print()
+ # Then the parsed results of all of them
+ print(_('Full PARSED config'))
+ for key in sorted( parsed_config.keys() ):
+ val = parsed_config[key]
+ # Filters are the last
+ if key == 'preproc' or key == 'postproc':
+ continue
+ # Flag beautifier
+ if key in FLAGS.keys() or key in ACTIONS.keys():
+ val = onoff.get(val) or val
+ # List beautifier
+ if isinstance(val, type([])):
+ if key == 'options': sep = ' '
+ else : sep = ', '
+ val = sep.join(val)
+ print("%25s: %s"%(dotted_spaces("%-14s"%key),val))
+ print()
+ print(_('Active filters'))
+ for filter_ in ['preproc', 'postproc']:
+ for rule in parsed_config.get(filter_) or []:
+ print("%25s: %s -> %s" % (
+ dotted_spaces("%-14s"%filter_), rule[0], rule[1]))
+
+
+def get_file_body(file_):
+ "Returns all the document BODY lines"
+ return process_source_file(file_, noconf=1)[1][2]
+
+
+def finish_him(outlist, config):
+ "Writing output to screen or file"
+ outfile = config['outfile']
+ outlist = unmaskEscapeChar(outlist)
+ outlist = expandLineBreaks(outlist)
+
+ # Apply PostProc filters
+ if config['postproc']:
+ filters = compile_filters(config['postproc'],
+ _('Invalid PostProc filter regex'))
+ postoutlist = []
+ errmsg = _('Invalid PostProc filter replacement')
+ for line in outlist:
+ for rgx,repl in filters:
+ try: line = rgx.sub(repl, line)
+ except: Error("%s: '%s'"%(errmsg, repl))
+ postoutlist.append(line)
+ outlist = postoutlist[:]
+
+ if outfile == MODULEOUT:
+ return outlist
+ elif outfile == STDOUT:
+ if GUI:
+ return outlist, config
+ else:
+ for line in outlist: print(line)
+ else:
+ Savefile(outfile, addLineBreaks(outlist))
+ if not GUI and not QUIET:
+ print(_('%s wrote %s')%(my_name,outfile))
+
+ if config['split']:
+ if not QUIET: print("--- html...")
+ sgml2html = 'sgml2html -s %s -l %s %s' % (
+ config['split'], config['lang'] or lang, outfile)
+ if not QUIET: print("Running system command:", sgml2html)
+ os.system(sgml2html)
+
+
+def toc_inside_body(body, toc, config):
+ ret = []
+ if AUTOTOC: return body # nothing to expand
+ toc_mark = MaskMaster().tocmask
+ # Expand toc mark with TOC contents
+ for line in body:
+ if line.count(toc_mark): # toc mark found
+ if config['toc']:
+ ret.extend(toc) # include if --toc
+ else:
+ pass # or remove %%toc line
+ else:
+ ret.append(line) # common line
+ return ret
+
+def toc_tagger(toc, config):
+ "Returns the tagged TOC, as a single tag or a tagged list"
+ ret = []
+ # Convert the TOC list (t2t-marked) to the target's list format
+ if config['toc-only'] or (config['toc'] and not TAGS['TOC']):
+ fakeconf = config.copy()
+ fakeconf['headers'] = 0
+ fakeconf['toc-only'] = 0
+ fakeconf['mask-email'] = 0
+ fakeconf['preproc'] = []
+ fakeconf['postproc'] = []
+ fakeconf['css-sugar'] = 0
+ fakeconf['art-no-title'] = 1 # needed for --toc and --slides together, avoids slide title before TOC
+ ret,foo = convert(toc, fakeconf)
+ set_global_config(config) # restore config
+ # Our TOC list is not needed, the target already knows how to do a TOC
+ elif config['toc'] and TAGS['TOC']:
+ ret = [TAGS['TOC']]
+ return ret
+
+def toc_formatter(toc, config):
+ "Formats TOC for automatic placement between headers and body"
+
+ if config['toc-only']: return toc # no formatting needed
+ if not config['toc'] : return [] # TOC disabled
+ ret = toc
+
+ # Art: An automatic "Table of Contents" header is added to the TOC slide
+ if config['target'] == 'art' and config['slides']:
+ n = (config['height'] - 1) - (len(toc) + 6) % (config['height'] - 1)
+ toc = aa_slide(_("Table of Contents"), config['width']) + toc + ([''] * n)
+ toc.append(aa_line(AA['bar2'], config['width']))
+ return toc
+
+ # TOC open/close tags (if any)
+ if TAGS['tocOpen' ]: ret.insert(0, TAGS['tocOpen'])
+ if TAGS['tocClose']: ret.append(TAGS['tocClose'])
+
+ # Autotoc specific formatting
+ if AUTOTOC:
+ if rules['autotocwithbars']: # TOC between bars
+ para = TAGS['paragraphOpen']+TAGS['paragraphClose']
+ bar = regex['x'].sub('-' * DFT_TEXT_WIDTH, TAGS['bar1'])
+ tocbar = [para, bar, para]
+ if config['target'] == 'art' and config['headers']:
+ # exception: header already printed a bar
+ ret = [para] + ret + tocbar
+ else:
+ ret = tocbar + ret + tocbar
+ if rules['blankendautotoc']: # blank line after TOC
+ ret.append('')
+ if rules['autotocnewpagebefore']: # page break before TOC
+ ret.insert(0,TAGS['pageBreak'])
+ if rules['autotocnewpageafter']: # page break after TOC
+ ret.append(TAGS['pageBreak'])
+ return ret
+
+
+def doHeader(headers, config):
+ if not config['headers']: return []
+ if not headers: headers = ['','','']
+ target = config['target']
+ if target not in HEADER_TEMPLATE:
+ Error("doHeader: Unknown target '%s'"%target)
+
+ if target in ('html','xhtml') and config.get('css-sugar'):
+ template = HEADER_TEMPLATE[target+'css'].split('\n')
+ else:
+ template = HEADER_TEMPLATE[target].split('\n')
+
+ head_data = {'STYLE':[], 'ENCODING':''}
+ for key in head_data.keys():
+ val = config.get(key.lower())
+ # Remove .sty extension from each style filename (freaking tex)
+ # XXX Can't handle --style foo.sty,bar.sty
+ if target == 'tex' and key == 'STYLE':
+ val = [re.sub('(?i)\.sty$','',x) for x in val]
+ if key == 'ENCODING':
+ val = get_encoding_string(val, target)
+ head_data[key] = val
+ # Parse header contents
+ for i in 0,1,2:
+ # Expand macros
+ contents = MacroMaster(config=config).expand(headers[i])
+ # Escapes - on tex, just do it if any \tag{} present
+ if target != 'tex' or \
+ (target == 'tex' and re.search(r'\\\w+{', contents)):
+ contents = doEscape(target, contents)
+ if target == 'lout':
+ contents = doFinalEscape(target, contents)
+
+ head_data['HEADER%d'%(i+1)] = contents
+
+ # css-inside removes STYLE line
+ #XXX In tex, this also removes the modules call (%!style:amsfonts)
+ if target in ('html','xhtml') and config.get('css-inside') and \
+ config.get('style'):
+ head_data['STYLE'] = []
+
+ Debug("Header Data: %s"%head_data, 1)
+
+ # ASCII Art does not use a header template, aa_header() formats the header
+ if target == 'art':
+ n_h = len([v for v in head_data if v.startswith("HEADER") and head_data[v]])
+ if not n_h :
+ return []
+ if config['slides']:
+ x = config['height'] - 3 - (n_h * 3)
+ n = x / (n_h + 1)
+ end = x % (n_h + 1)
+ template = aa_header(head_data, config['width'], n, end)
+ else:
+ template = [''] + aa_header(head_data, config['width'], 2, 0)
+ # Header done, let's get out
+ return template
+
+ # Scan for empty dictionary keys
+ # If found, scan template lines for that key reference
+ # If found, remove the reference
+ # If there isn't any other key reference on the same line, remove it
+ #TODO loop by template line > key
+ for key in head_data.keys():
+ if head_data.get(key): continue
+ for line in template:
+ if line.count('%%(%s)s'%key):
+ sline = line.replace('%%(%s)s'%key, '')
+ if not re.search(r'%\([A-Z0-9]+\)s', sline):
+ template.remove(line)
+ # Style is a multiple tag.
+ # - If none or just one, use default template
+ # - If two or more, insert extra lines in a loop (and remove original)
+ styles = head_data['STYLE']
+ if len(styles) == 1:
+ head_data['STYLE'] = styles[0]
+ elif len(styles) > 1:
+ style_mark = '%(STYLE)s'
+ for i in range(len(template)):
+ if template[i].count(style_mark):
+ while styles:
+ template.insert(i+1, template[i].replace(style_mark, styles.pop()))
+ del template[i]
+ break
+ # Populate template with data (dict expansion)
+ template = '\n'.join(template) % head_data
+
+ # Adding CSS contents into template (for --css-inside)
+ # This code sux. Dirty++
+ if target in ('html','xhtml') and config.get('css-inside') and \
+ config.get('style'):
+ set_global_config(config) # usually on convert(), needed here
+ for i in range(len(config['style'])):
+ cssfile = config['style'][i]
+ if not os.path.isabs(cssfile):
+ infile = config.get('sourcefile')
+ cssfile = os.path.join(
+ os.path.dirname(infile), cssfile)
+ try:
+ contents = Readfile(cssfile, 1)
+ css = "\n%s\n%s\n%s\n%s\n" % (
+ doCommentLine("Included %s" % cssfile),
+ TAGS['cssOpen'],
+ '\n'.join(contents),
+ TAGS['cssClose'])
+ # Style now is content, needs escaping (tex)
+ #css = maskEscapeChar(css)
+ except:
+ errmsg = "CSS include failed for %s" % cssfile
+ css = "\n%s\n" % (doCommentLine(errmsg))
+ # Insert this CSS file contents on the template
+ template = re.sub('(?i)()', css+r'\1', template)
+ # template = re.sub(r'(?i)(\\begin{document})',
+ # css+'\n'+r'\1', template) # tex
+
+ # The last blank line to keep everything separated
+ template = re.sub('(?i)()', '\n'+r'\1', template)
+
+ return template.split('\n')
+
+def doCommentLine(txt):
+ # The -- string ends a (h|sg|xht)ml comment :(
+ txt = maskEscapeChar(txt)
+ if TAGS['comment'].count('--') and txt.count('--'):
+ txt = re.sub('-(?=-)', r'-\\', txt)
+
+ if TAGS['comment']:
+ return regex['x'].sub(txt, TAGS['comment'])
+ return ''
+
+def doFooter(config):
+ ret = []
+
+ # No footer. The --no-headers option hides header AND footer
+ if not config['headers']:
+ return []
+
+ # Only add blank line before footer if last block doesn't added by itself
+ if not rules.get('blanksaround'+BLOCK.last):
+ ret.append('')
+
+ # Add txt2tags info at footer, if target supports comments
+ if TAGS['comment']:
+
+ # Not using TARGET_NAMES because it's i18n'ed.
+ # It's best to always present this info in english.
+ target = config['target']
+ if config['target'] == 'tex':
+ target = 'LaTeX2e'
+
+ t2t_version = '%s code generated by %s %s (%s)' % (target, my_name, my_version, my_url)
+ cmdline = 'cmdline: %s %s' % (my_name, ' '.join(config['realcmdline']))
+
+ ret.append(doCommentLine(t2t_version))
+ ret.append(doCommentLine(cmdline))
+
+ # Maybe we have a specific tag to close the document?
+ if TAGS['EOD']:
+ ret.append(TAGS['EOD'])
+
+ return ret
+
+def doEscape(target,txt):
+ "Target-specific special escapes. Apply *before* insert any tag."
+ tmpmask = 'vvvvThisEscapingSuxvvvv'
+ if target in ('html','sgml','xhtml','dbk'):
+ txt = re.sub('&','&',txt)
+ txt = re.sub('<','<',txt)
+ txt = re.sub('>','>',txt)
+ if target == 'sgml':
+ txt = re.sub('\xff','ÿ',txt) # "+y
+ elif target == 'pm6':
+ txt = re.sub('<','<\#60>',txt)
+ elif target == 'mgp':
+ txt = re.sub('^%',' %',txt) # add leading blank to avoid parse
+ elif target == 'man':
+ txt = re.sub("^([.'])", '\\&\\1',txt) # command ID
+ txt = txt.replace(ESCCHAR, ESCCHAR+'e') # \e
+ elif target == 'lout':
+ # TIP: / moved to FinalEscape to avoid //italic//
+ # TIP: these are also converted by lout: ... --- --
+ txt = txt.replace(ESCCHAR, tmpmask) # \
+ txt = txt.replace('"', '"%s""'%ESCCHAR) # "\""
+ txt = re.sub('([|&{}@#^~])', '"\\1"', txt) # "@"
+ txt = txt.replace(tmpmask, '"%s"'%(ESCCHAR*2)) # "\\"
+ elif target == 'tex':
+ # Mark literal \ to be changed to $\backslash$ later
+ txt = txt.replace(ESCCHAR, tmpmask)
+ txt = re.sub('([#$&%{}])', ESCCHAR+r'\1' , txt) # \%
+ txt = re.sub('([~^])' , ESCCHAR+r'\1{}', txt) # \~{}
+ txt = re.sub('([<|>])' , r'$\1$', txt) # $>$
+ txt = txt.replace(tmpmask, maskEscapeChar(r'$\backslash$'))
+ # TIP the _ is escaped at the end
+ return txt
+
+# TODO man: where - really needs to be escaped?
+def doFinalEscape(target, txt):
+ "Last escapes of each line"
+ if target == 'pm6' : txt = txt.replace(ESCCHAR+'<', r'<\#92><')
+ elif target == 'man' : txt = txt.replace('-', r'\-')
+ elif target == 'sgml': txt = txt.replace('[', '[')
+ elif target == 'lout': txt = txt.replace('/', '"/"')
+ elif target == 'tex' :
+ txt = txt.replace('_', r'\_')
+ txt = txt.replace('vvvvTexUndervvvv', '_') # shame!
+ return txt
+
+def EscapeCharHandler(action, data):
+ "Mask/Unmask the Escape Char on the given string"
+ if not data.strip(): return data
+ if action not in ('mask','unmask'):
+ Error("EscapeCharHandler: Invalid action '%s'"%action)
+ if action == 'mask': return data.replace('\\', ESCCHAR)
+ else: return data.replace(ESCCHAR, '\\')
+
+def maskEscapeChar(data):
+ "Replace any Escape Char \ with a text mask (Input: str or list)"
+ if isinstance(data, type([])):
+ return [EscapeCharHandler('mask', x) for x in data]
+ return EscapeCharHandler('mask',data)
+
+def unmaskEscapeChar(data):
+ "Undo the Escape char \ masking (Input: str or list)"
+ if isinstance(data, type([])):
+ return [EscapeCharHandler('unmask', x) for x in data]
+ return EscapeCharHandler('unmask',data)
+
+def addLineBreaks(mylist):
+ "use LB to respect sys.platform"
+ ret = []
+ for line in mylist:
+ line = line.replace('\n', LB) # embedded \n's
+ ret.append(line+LB) # add final line break
+ return ret
+
+# Convert ['foo\nbar'] to ['foo', 'bar']
+def expandLineBreaks(mylist):
+ ret = []
+ for line in mylist:
+ ret.extend(line.split('\n'))
+ return ret
+
+def compile_filters(filters, errmsg='Filter'):
+ if filters:
+ for i in range(len(filters)):
+ patt,repl = filters[i]
+ try: rgx = re.compile(patt)
+ except: Error("%s: '%s'"%(errmsg, patt))
+ filters[i] = (rgx,repl)
+ return filters
+
+def enclose_me(tagname, txt):
+ return TAGS.get(tagname+'Open') + txt + TAGS.get(tagname+'Close')
+
+def beautify_me(name, line):
+ "where name is: bold, italic, underline or strike"
+
+ # Exception: Doesn't parse an horizontal bar as strike
+ if name == 'strike' and regex['bar'].search(line): return line
+
+ name = 'font%s' % name.capitalize()
+ open_ = TAGS['%sOpen'%name]
+ close = TAGS['%sClose'%name]
+ txt = r'%s\1%s'%(open_, close)
+ line = regex[name].sub(txt,line)
+ return line
+
+def get_tagged_link(label, url):
+ ret = ''
+ target = CONF['target']
+ image_re = regex['img']
+
+ # Set link type
+ if regex['email'].match(url):
+ linktype = 'email'
+ else:
+ linktype = 'url';
+
+ # Escape specials from TEXT parts
+ label = doEscape(target,label)
+
+ # Escape specials from link URL
+ if not rules['linkable'] or rules['escapeurl']:
+ url = doEscape(target, url)
+
+ # Adding protocol to guessed link
+ guessurl = ''
+ if linktype == 'url' and \
+ re.match('(?i)'+regex['_urlskel']['guess'], url):
+ if url[0] in 'Ww': guessurl = 'http://' +url
+ else : guessurl = 'ftp://' +url
+
+ # Not link aware targets -> protocol is useless
+ if not rules['linkable']: guessurl = ''
+
+ # Simple link (not guessed)
+ if not label and not guessurl:
+ if CONF['mask-email'] and linktype == 'email':
+ # Do the email mask feature (no TAGs, just text)
+ url = url.replace('@', ' (a) ')
+ url = url.replace('.', ' ')
+ url = "<%s>" % url
+ if rules['linkable']: url = doEscape(target, url)
+ ret = url
+ else:
+ # Just add link data to tag
+ tag = TAGS[linktype]
+ ret = regex['x'].sub(url,tag)
+
+ # Named link or guessed simple link
+ else:
+ # Adjusts for guessed link
+ if not label: label = url # no protocol
+ if guessurl : url = guessurl # with protocol
+
+ # Image inside link!
+ if image_re.match(label):
+ if rules['imglinkable']: # get image tag
+ label = parse_images(label)
+ else: # img@link !supported
+ label = "(%s)"%image_re.match(label).group(1)
+
+ # Putting data on the right appearance order
+ if rules['labelbeforelink'] or not rules['linkable']:
+ urlorder = [label, url] # label before link
+ else:
+ urlorder = [url, label] # link before label
+
+ # Add link data to tag (replace \a's)
+ ret = TAGS["%sMark"%linktype]
+ for data in urlorder:
+ ret = regex['x'].sub(data,ret,1)
+
+ return ret
+
+
+def parse_deflist_term(line):
+ "Extract and parse definition list term contents"
+ img_re = regex['img']
+ term = regex['deflist'].search(line).group(3)
+
+ # Mask image inside term as (image.jpg), where not supported
+ if not rules['imgasdefterm'] and img_re.search(term):
+ while img_re.search(term):
+ imgfile = img_re.search(term).group(1)
+ term = img_re.sub('(%s)'%imgfile, term, 1)
+
+ #TODO tex: escape ] on term. \], \rbrack{} and \verb!]! don't work :(
+ return term
+
+
+def get_image_align(line):
+ "Return the image (first found) align for the given line"
+
+ # First clear marks that can mess align detection
+ line = re.sub(SEPARATOR+'$', '', line) # remove deflist sep
+ line = re.sub('^'+SEPARATOR, '', line) # remove list sep
+ line = re.sub('^[\t]+' , '', line) # remove quote mark
+
+ # Get image position on the line
+ m = regex['img'].search(line)
+ ini = m.start() ; head = 0
+ end = m.end() ; tail = len(line)
+
+ # The align detection algorithm
+ if ini == head and end != tail: align = 'left' # ^img + text$
+ elif ini != head and end == tail: align = 'right' # ^text + img$
+ else : align = 'center' # default align
+
+ # Some special cases
+ if BLOCK.isblock('table'): align = 'center' # ignore when table
+# if TARGET == 'mgp' and align == 'center': align = 'center'
+
+ return align
+
+
+# Reference: http://www.iana.org/assignments/character-sets
+# http://www.drclue.net/F1.cgi/HTML/META/META.html
+def get_encoding_string(enc, target):
+ if not enc: return ''
+ # Target specific translation table
+ translate = {
+ 'tex': {
+ # missing: ansinew , applemac , cp437 , cp437de , cp865
+ 'utf-8' : 'utf8',
+ 'us-ascii' : 'ascii',
+ 'windows-1250': 'cp1250',
+ 'windows-1252': 'cp1252',
+ 'ibm850' : 'cp850',
+ 'ibm852' : 'cp852',
+ 'iso-8859-1' : 'latin1',
+ 'iso-8859-2' : 'latin2',
+ 'iso-8859-3' : 'latin3',
+ 'iso-8859-4' : 'latin4',
+ 'iso-8859-5' : 'latin5',
+ 'iso-8859-9' : 'latin9',
+ 'koi8-r' : 'koi8-r'
+ }
+ }
+ # Normalization
+ enc = re.sub('(?i)(us[-_]?)?ascii|us|ibm367','us-ascii' , enc)
+ enc = re.sub('(?i)(ibm|cp)?85([02])' ,'ibm85\\2' , enc)
+ enc = re.sub('(?i)(iso[_-]?)?8859[_-]?' ,'iso-8859-' , enc)
+ enc = re.sub('iso-8859-($|[^1-9]).*' ,'iso-8859-1', enc)
+ # Apply translation table
+ try: enc = translate[target][enc.lower()]
+ except: pass
+ return enc
+
+
+##############################################################################
+##MerryChristmas,IdontwanttofighttonightwithyouImissyourbodyandIneedyourlove##
+##############################################################################
+
+
+def process_source_file(file_='', noconf=0, contents=[]):
+ """
+ Find and Join all the configuration available for a source file.
+ No sanity checking is done on this step.
+ It also extracts the source document parts into separate holders.
+
+ The config scan order is:
+ 1. The user configuration file (i.e. $HOME/.txt2tagsrc)
+ 2. The source document's CONF area
+ 3. The command line options
+
+ The return data is a tuple of two items:
+ 1. The parsed config dictionary
+ 2. The document's parts, as a (head, conf, body) tuple
+
+ All the conversion process will be based on the data and
+ configuration returned by this function.
+ The source files is read on this step only.
+ """
+ if contents:
+ source = SourceDocument(contents=contents)
+ else:
+ source = SourceDocument(file_)
+ head, conf, body = source.split()
+ Message(_("Source document contents stored"),2)
+ if not noconf:
+ # Read document config
+ source_raw = source.get_raw_config()
+ # Join all the config directives found, then parse it
+ full_raw = RC_RAW + source_raw + CMDLINE_RAW
+ Message(_("Parsing and saving all config found (%03d items)") % (len(full_raw)), 1)
+ full_parsed = ConfigMaster(full_raw).parse()
+ # Add manually the filename to the conf dic
+ if contents:
+ full_parsed['sourcefile'] = MODULEIN
+ full_parsed['infile'] = MODULEIN
+ full_parsed['outfile'] = MODULEOUT
+ else:
+ full_parsed['sourcefile'] = file_
+ # Maybe should we dump the config found?
+ if full_parsed.get('dump-config'):
+ dumpConfig(source_raw, full_parsed)
+ Quit()
+ # The user just want to know a single config value (hidden feature)
+ #TODO pick a better name than --show-config-value
+ elif full_parsed.get('show-config-value'):
+ config_value = full_parsed.get(full_parsed['show-config-value'])
+ if config_value:
+ if isinstance(config_value, type([])):
+ print('\n'.join(config_value))
+ else:
+ print(config_value)
+ Quit()
+ # Okay, all done
+ Debug("FULL config for this file: %s"%full_parsed, 1)
+ else:
+ full_parsed = {}
+ return full_parsed, (head,conf,body)
+
+def get_infiles_config(infiles):
+ """
+ Find and Join into a single list, all configuration available
+ for each input file. This function is supposed to be the very
+ first one to be called, before any processing.
+ """
+ return list(map(process_source_file, infiles))
+
+def convert_this_files(configs):
+ global CONF
+ for myconf,doc in configs: # multifile support
+ target_head = []
+ target_toc = []
+ target_body = []
+ target_foot = []
+ source_head, source_conf, source_body = doc
+ myconf = ConfigMaster().sanity(myconf)
+ # Compose the target file Headers
+ #TODO escape line before?
+ #TODO see exceptions by tex and mgp
+ Message(_("Composing target Headers"),1)
+ target_head = doHeader(source_head, myconf)
+ # Parse the full marked body into tagged target
+ first_body_line = (len(source_head) or 1)+ len(source_conf) + 1
+ Message(_("Composing target Body"),1)
+ target_body, marked_toc = convert(source_body, myconf, firstlinenr=first_body_line)
+ # If dump-source, we're done
+ if myconf['dump-source']:
+ for line in source_head+source_conf+target_body:
+ print(line)
+ return
+
+ # Close the last slide
+ if myconf['slides'] and not myconf['toc-only'] and myconf['target'] == 'art':
+ n = (myconf['height'] - 1) - (AA_COUNT % (myconf['height'] - 1) + 1)
+ target_body = target_body + ([''] * n) + [aa_line(AA['bar2'], myconf['width'])]
+
+ # Compose the target file Footer
+ Message(_("Composing target Footer"),1)
+ target_foot = doFooter(myconf)
+
+ # Make TOC (if needed)
+ Message(_("Composing target TOC"),1)
+ tagged_toc = toc_tagger(marked_toc, myconf)
+ target_toc = toc_formatter(tagged_toc, myconf)
+ target_body = toc_inside_body(target_body, target_toc, myconf)
+ if not AUTOTOC and not myconf['toc-only']: target_toc = []
+ # Finally, we have our document
+ outlist = target_head + target_toc + target_body + target_foot
+ # If on GUI, abort before finish_him
+ # If module, return finish_him as list
+ # Else, write results to file or STDOUT
+ if GUI:
+ return outlist, myconf
+ elif myconf.get('outfile') == MODULEOUT:
+ return finish_him(outlist, myconf), myconf
+ else:
+ Message(_("Saving results to the output file"),1)
+ finish_him(outlist, myconf)
+
+
+def parse_images(line):
+ "Tag all images found"
+ while regex['img'].search(line) and TAGS['img'] != '[\a]':
+ txt = regex['img'].search(line).group(1)
+ tag = TAGS['img']
+
+ # If target supports image alignment, here we go
+ if rules['imgalignable']:
+
+ align = get_image_align(line) # right
+ align_name = align.capitalize() # Right
+
+ # The align is a full tag, or part of the image tag (~A~)
+ if TAGS['imgAlign'+align_name]:
+ tag = TAGS['imgAlign'+align_name]
+ else:
+ align_tag = TAGS['_imgAlign'+align_name]
+ tag = regex['_imgAlign'].sub(align_tag, tag, 1)
+
+ # Dirty fix to allow centered solo images
+ if align == 'center' and TARGET in ('html','xhtml'):
+ rest = regex['img'].sub('',line,1)
+ if re.match('^\s+$', rest):
+ tag = "
%s" %tag
+
+ if TARGET == 'tex':
+ tag = re.sub(r'\\b',r'\\\\b',tag)
+ txt = txt.replace('_', 'vvvvTexUndervvvv')
+
+ # Ugly hack to avoid infinite loop when target's image tag contains []
+ tag = tag.replace('[', 'vvvvEscapeSquareBracketvvvv')
+
+ line = regex['img'].sub(tag,line,1)
+ line = regex['x'].sub(txt,line,1)
+ return line.replace('vvvvEscapeSquareBracketvvvv','[')
+
+
+def add_inline_tags(line):
+ # Beautifiers
+ for beauti in ('bold', 'italic', 'underline', 'strike'):
+ if regex['font%s'%beauti.capitalize()].search(line):
+ line = beautify_me(beauti, line)
+
+ line = parse_images(line)
+ return line
+
+
+def get_include_contents(file_, path=''):
+ "Parses %!include: value and extract file contents"
+ ids = {'`':'verb', '"':'raw', "'":'tagged' }
+ id_ = 't2t'
+ # Set include type and remove identifier marks
+ mark = file_[0]
+ if mark in ids.keys():
+ if file_[:2] == file_[-2:] == mark*2:
+ id_ = ids[mark] # set type
+ file_ = file_[2:-2] # remove marks
+ # Handle remote dir execution
+ filepath = os.path.join(path, file_)
+ # Read included file contents
+ lines = Readfile(filepath, remove_linebreaks=1)
+ # Default txt2tags marked text, just BODY matters
+ if id_ == 't2t':
+ lines = get_file_body(filepath)
+ #TODO fix images relative path if file has a path, ie.: chapter1/index.t2t (wait until tree parsing)
+ #TODO for the images path fix, also respect outfile path, if different from infile (wait until tree parsing)
+ lines.insert(0, '%%INCLUDED(%s) starts here: %s'%(id_,file_))
+ # This appears when included hit EOF with verbatim area open
+ #lines.append('%%INCLUDED(%s) ends here: %s'%(id_,file_))
+ return id_, lines
+
+
+def set_global_config(config):
+ global CONF, TAGS, regex, rules, TARGET
+ CONF = config
+ rules = getRules(CONF)
+ TAGS = getTags(CONF)
+ regex = getRegexes()
+ TARGET = config['target'] # save for buggy functions that need global
+
+
+def convert(bodylines, config, firstlinenr=1):
+ global BLOCK, TITLE
+
+ set_global_config(config)
+
+ target = config['target']
+ BLOCK = BlockMaster()
+ MASK = MaskMaster()
+ TITLE = TitleMaster()
+
+ ret = []
+ dump_source = []
+ f_lastwasblank = 0
+
+ # Compiling all PreProc regexes
+ pre_filter = compile_filters(
+ CONF['preproc'], _('Invalid PreProc filter regex'))
+
+ # Let's mark it up!
+ linenr = firstlinenr-1
+ lineref = 0
+ while lineref < len(bodylines):
+ # Defaults
+ MASK.reset()
+ results_box = ''
+
+ untouchedline = bodylines[lineref]
+ dump_source.append(untouchedline)
+
+ line = re.sub('[\n\r]+$','',untouchedline) # del line break
+
+ # Apply PreProc filters
+ if pre_filter:
+ errmsg = _('Invalid PreProc filter replacement')
+ for rgx,repl in pre_filter:
+ try: line = rgx.sub(repl, line)
+ except: Error("%s: '%s'"%(errmsg, repl))
+
+ line = maskEscapeChar(line) # protect \ char
+ linenr += 1
+ lineref += 1
+
+ Debug(repr(line), 2, linenr) # heavy debug: show each line
+
+ #------------------[ Comment Block ]------------------------
+
+ # We're already on a comment block
+ if BLOCK.block() == 'comment':
+
+ # Closing comment
+ if regex['blockCommentClose'].search(line):
+ ret.extend(BLOCK.blockout() or [])
+ continue
+
+ # Normal comment-inside line. Ignore it.
+ continue
+
+ # Detecting comment block init
+ if regex['blockCommentOpen'].search(line) \
+ and BLOCK.block() not in BLOCK.exclusive:
+ ret.extend(BLOCK.blockin('comment'))
+ continue
+
+ #-------------------------[ Tagged Text ]----------------------
+
+ # We're already on a tagged block
+ if BLOCK.block() == 'tagged':
+
+ # Closing tagged
+ if regex['blockTaggedClose'].search(line):
+ ret.extend(BLOCK.blockout())
+ continue
+
+ # Normal tagged-inside line
+ BLOCK.holdadd(line)
+ continue
+
+ # Detecting tagged block init
+ if regex['blockTaggedOpen'].search(line) \
+ and BLOCK.block() not in BLOCK.exclusive:
+ ret.extend(BLOCK.blockin('tagged'))
+ continue
+
+ # One line tagged text
+ if regex['1lineTagged'].search(line) \
+ and BLOCK.block() not in BLOCK.exclusive:
+ ret.extend(BLOCK.blockin('tagged'))
+ line = regex['1lineTagged'].sub('',line)
+ BLOCK.holdadd(line)
+ ret.extend(BLOCK.blockout())
+ continue
+
+ #-------------------------[ Raw Text ]----------------------
+
+ # We're already on a raw block
+ if BLOCK.block() == 'raw':
+
+ # Closing raw
+ if regex['blockRawClose'].search(line):
+ ret.extend(BLOCK.blockout())
+ continue
+
+ # Normal raw-inside line
+ BLOCK.holdadd(line)
+ continue
+
+ # Detecting raw block init
+ if regex['blockRawOpen'].search(line) \
+ and BLOCK.block() not in BLOCK.exclusive:
+ ret.extend(BLOCK.blockin('raw'))
+ continue
+
+ # One line raw text
+ if regex['1lineRaw'].search(line) \
+ and BLOCK.block() not in BLOCK.exclusive:
+ ret.extend(BLOCK.blockin('raw'))
+ line = regex['1lineRaw'].sub('',line)
+ BLOCK.holdadd(line)
+ ret.extend(BLOCK.blockout())
+ continue
+
+ #------------------------[ Verbatim ]----------------------
+
+ #TIP We'll never support beautifiers inside verbatim
+
+ # Closing table mapped to verb
+ if BLOCK.block() == 'verb' \
+ and BLOCK.prop('mapped') == 'table' \
+ and not regex['table'].search(line):
+ ret.extend(BLOCK.blockout())
+
+ # We're already on a verb block
+ if BLOCK.block() == 'verb':
+
+ # Closing verb
+ if regex['blockVerbClose'].search(line):
+ ret.extend(BLOCK.blockout())
+ continue
+
+ # Normal verb-inside line
+ BLOCK.holdadd(line)
+ continue
+
+ # Detecting verb block init
+ if regex['blockVerbOpen'].search(line) \
+ and BLOCK.block() not in BLOCK.exclusive:
+ ret.extend(BLOCK.blockin('verb'))
+ f_lastwasblank = 0
+ continue
+
+ # One line verb-formatted text
+ if regex['1lineVerb'].search(line) \
+ and BLOCK.block() not in BLOCK.exclusive:
+ ret.extend(BLOCK.blockin('verb'))
+ line = regex['1lineVerb'].sub('',line)
+ BLOCK.holdadd(line)
+ ret.extend(BLOCK.blockout())
+ f_lastwasblank = 0
+ continue
+
+ # Tables are mapped to verb when target is not table-aware
+ if not rules['tableable'] and regex['table'].search(line):
+ if not BLOCK.isblock('verb'):
+ ret.extend(BLOCK.blockin('verb'))
+ BLOCK.propset('mapped', 'table')
+ BLOCK.holdadd(line)
+ continue
+
+ #---------------------[ blank lines ]-----------------------
+
+ if regex['blankline'].search(line):
+
+ # Close open paragraph
+ if BLOCK.isblock('para'):
+ ret.extend(BLOCK.blockout())
+ f_lastwasblank = 1
+ continue
+
+ # Close all open tables
+ if BLOCK.isblock('table'):
+ ret.extend(BLOCK.blockout())
+ f_lastwasblank = 1
+ continue
+
+ # Close all open quotes
+ while BLOCK.isblock('quote'):
+ ret.extend(BLOCK.blockout())
+
+ # Closing all open lists
+ if f_lastwasblank: # 2nd consecutive blank
+ if BLOCK.block().endswith('list'):
+ BLOCK.holdaddsub('') # helps parser
+ while BLOCK.depth: # closes list (if any)
+ ret.extend(BLOCK.blockout())
+ continue # ignore consecutive blanks
+
+ # Paragraph (if any) is wanted inside lists also
+ if BLOCK.block().endswith('list'):
+ BLOCK.holdaddsub('')
+
+ f_lastwasblank = 1
+ continue
+
+
+ #---------------------[ special ]---------------------------
+
+ if regex['special'].search(line):
+
+ targ, key, val = ConfigLines().parse_line(line, None, target)
+
+ if key:
+ Debug("Found config '%s', value '%s'" % (key, val), 1, linenr)
+ else:
+ Debug('Bogus Special Line', 1, linenr)
+
+ # %!include command
+ if key == 'include':
+
+ incpath = os.path.dirname(CONF['sourcefile'])
+ incfile = val
+ err = _('A file cannot include itself (loop!)')
+ if CONF['sourcefile'] == incfile:
+ Error("%s: %s"%(err,incfile))
+ inctype, inclines = get_include_contents(incfile, incpath)
+
+ # Verb, raw and tagged are easy
+ if inctype != 't2t':
+ ret.extend(BLOCK.blockin(inctype))
+ BLOCK.holdextend(inclines)
+ ret.extend(BLOCK.blockout())
+ else:
+ # Insert include lines into body
+ #TODO include maxdepth limit
+ bodylines = bodylines[:lineref] + inclines + bodylines[lineref:]
+ #TODO fix path if include@include
+ # Remove %!include call
+ if CONF['dump-source']:
+ dump_source.pop()
+
+ # This line is done, go to next
+ continue
+
+ # %!csv command
+ elif key == 'csv':
+
+ if not csv:
+ Error("Python module 'csv' not found, but needed for %!csv")
+
+ table = []
+ filename = val
+ reader = csv.reader(Readfile(filename))
+
+ # Convert each CSV line to a txt2tags' table line
+ # foo,bar,baz -> | foo | bar | baz |
+ try:
+ for row in reader:
+ table.append('| %s |' % ' | '.join(row))
+ except csv.Error as e:
+ Error('CSV: file %s: %s' % (filename, e))
+
+ # Parse and convert the new table
+ # Note: cell contents is raw, no t2t marks are parsed
+ if rules['tableable']:
+ ret.extend(BLOCK.blockin('table'))
+ if table:
+ BLOCK.tableparser.__init__(table[0])
+ for row in table:
+ tablerow = TableMaster().parse_row(row)
+ BLOCK.tableparser.add_row(tablerow)
+
+ # Very ugly, but necessary for escapes
+ line = SEPARATOR.join(tablerow['cells'])
+ BLOCK.holdadd(doEscape(target, line))
+ ret.extend(BLOCK.blockout())
+
+ # Tables are mapped to verb when target is not table-aware
+ else:
+ if target == 'art' and table:
+ table = aa_table(table)
+ ret.extend(BLOCK.blockin('verb'))
+ BLOCK.propset('mapped', 'table')
+ for row in table:
+ BLOCK.holdadd(row)
+ ret.extend(BLOCK.blockout())
+
+ # This line is done, go to next
+ continue
+
+ #---------------------[ dump-source ]-----------------------
+
+ # We don't need to go any further
+ if CONF['dump-source']:
+ continue
+
+ #---------------------[ Comments ]--------------------------
+
+ # Just skip them (if not macro)
+ if regex['comment'].search(line) and not \
+ regex['macros'].match(line) and not \
+ regex['toc'].match(line):
+ continue
+
+ #---------------------[ Triggers ]--------------------------
+
+ # Valid line, reset blank status
+ f_lastwasblank = 0
+
+ # Any NOT quote line closes all open quotes
+ if BLOCK.isblock('quote') and not regex['quote'].search(line):
+ while BLOCK.isblock('quote'):
+ ret.extend(BLOCK.blockout())
+
+ # Any NOT table line closes an open table
+ if BLOCK.isblock('table') and not regex['table'].search(line):
+ ret.extend(BLOCK.blockout())
+
+
+ #---------------------[ Horizontal Bar ]--------------------
+
+ if regex['bar'].search(line):
+
+ # Bars inside quotes are handled on the Quote processing
+ # Otherwise we parse the bars right here
+ #
+ if not (BLOCK.isblock('quote') or regex['quote'].search(line)) \
+ or (BLOCK.isblock('quote') and not rules['barinsidequote']):
+
+ # Close all the opened blocks
+ ret.extend(BLOCK.blockin('bar'))
+
+ # Extract the bar chars (- or =)
+ m = regex['bar'].search(line)
+ bar_chars = m.group(2)
+
+ # Process and dump the tagged bar
+ BLOCK.holdadd(bar_chars)
+ ret.extend(BLOCK.blockout())
+ Debug("BAR: %s"%line, 6)
+
+ # We're done, nothing more to process
+ continue
+
+
+ #---------------------[ Title ]-----------------------------
+
+ if (regex['title'].search(line) or regex['numtitle'].search(line)) \
+ and not BLOCK.block().endswith('list'):
+
+ if regex['title'].search(line):
+ name = 'title'
+ else:
+ name = 'numtitle'
+
+ # Close all the opened blocks
+ ret.extend(BLOCK.blockin(name))
+
+ # Process title
+ TITLE.add(line)
+ ret.extend(BLOCK.blockout())
+
+ # We're done, nothing more to process
+ continue
+
+ #---------------------[ %%toc ]-----------------------
+
+ # %%toc line closes paragraph
+ if BLOCK.block() == 'para' and regex['toc'].search(line):
+ ret.extend(BLOCK.blockout())
+
+ #---------------------[ apply masks ]-----------------------
+
+ line = MASK.mask(line)
+
+ #XXX from here, only block-inside lines will pass
+
+ #---------------------[ Quote ]-----------------------------
+
+ if regex['quote'].search(line):
+
+ # Store number of leading TABS
+ quotedepth = len(regex['quote'].search(line).group(0))
+
+ # SGML doesn't support nested quotes
+ if rules['quotenotnested']: quotedepth = 1
+
+ # Don't cross depth limit
+ maxdepth = rules['quotemaxdepth']
+ if maxdepth and quotedepth > maxdepth:
+ quotedepth = maxdepth
+
+ # New quote
+ if not BLOCK.isblock('quote'):
+ ret.extend(BLOCK.blockin('quote'))
+
+ # New subquotes
+ while BLOCK.depth < quotedepth:
+ BLOCK.blockin('quote')
+
+ # Closing quotes
+ while quotedepth < BLOCK.depth:
+ ret.extend(BLOCK.blockout())
+
+ # Bar inside quote
+ if regex['bar'].search(line) and rules['barinsidequote']:
+ tempBlock = BlockMaster()
+ tagged_bar = []
+ tagged_bar.extend(tempBlock.blockin('bar'))
+ tempBlock.holdadd(line)
+ tagged_bar.extend(tempBlock.blockout())
+ BLOCK.holdextend(tagged_bar)
+ continue
+
+ #---------------------[ Lists ]-----------------------------
+
+ # An empty item also closes the current list
+ if BLOCK.block().endswith('list'):
+ m = regex['listclose'].match(line)
+ if m:
+ listindent = m.group(1)
+ listtype = m.group(2)
+ currlisttype = BLOCK.prop('type')
+ currlistindent = BLOCK.prop('indent')
+ if listindent == currlistindent and \
+ listtype == currlisttype:
+ ret.extend(BLOCK.blockout())
+ continue
+
+ if regex['list'].search(line) or \
+ regex['numlist'].search(line) or \
+ regex['deflist'].search(line):
+
+ listindent = BLOCK.prop('indent')
+ listids = ''.join(LISTNAMES.keys())
+ m = re.match('^( *)([%s]) '%listids, line)
+ listitemindent = m.group(1)
+ listtype = m.group(2)
+ listname = LISTNAMES[listtype]
+ results_box = BLOCK.holdadd
+
+ # Del list ID (and separate term from definition)
+ if listname == 'deflist':
+ term = parse_deflist_term(line)
+ line = regex['deflist'].sub(
+ SEPARATOR+term+SEPARATOR,line)
+ else:
+ line = regex[listname].sub(SEPARATOR,line)
+
+ # Don't cross depth limit
+ maxdepth = rules['listmaxdepth']
+ if maxdepth and BLOCK.depth == maxdepth:
+ if len(listitemindent) > len(listindent):
+ listitemindent = listindent
+
+ # List bumping (same indent, diff mark)
+ # Close the currently open list to clear the mess
+ if BLOCK.block().endswith('list') \
+ and listname != BLOCK.block() \
+ and len(listitemindent) == len(listindent):
+ ret.extend(BLOCK.blockout())
+ listindent = BLOCK.prop('indent')
+
+ # Open mother list or sublist
+ if not BLOCK.block().endswith('list') or \
+ len(listitemindent) > len(listindent):
+ ret.extend(BLOCK.blockin(listname))
+ BLOCK.propset('indent',listitemindent)
+ BLOCK.propset('type',listtype)
+
+ # Closing sublists
+ while len(listitemindent) < len(BLOCK.prop('indent')):
+ ret.extend(BLOCK.blockout())
+
+ # O-oh, sublist before list ("\n\n - foo\n- foo")
+ # Fix: close sublist (as mother), open another list
+ if not BLOCK.block().endswith('list'):
+ ret.extend(BLOCK.blockin(listname))
+ BLOCK.propset('indent',listitemindent)
+ BLOCK.propset('type',listtype)
+
+ #---------------------[ Table ]-----------------------------
+
+ #TODO escape undesired format inside table
+ #TODO add pm6 target
+ if regex['table'].search(line):
+
+ if not BLOCK.isblock('table'): # first table line!
+ ret.extend(BLOCK.blockin('table'))
+ BLOCK.tableparser.__init__(line)
+
+ tablerow = TableMaster().parse_row(line)
+ BLOCK.tableparser.add_row(tablerow) # save config
+
+ # Maintain line to unmask and inlines
+ # XXX Bug: | **bo | ld** | turns **bo\x01ld** and gets converted :(
+ # TODO isolate unmask+inlines parsing to use here
+ line = SEPARATOR.join(tablerow['cells'])
+
+ #---------------------[ Paragraph ]-------------------------
+
+ if not BLOCK.block() and \
+ not line.count(MASK.tocmask): # new para!
+ ret.extend(BLOCK.blockin('para'))
+
+
+ ############################################################
+ ############################################################
+ ############################################################
+
+
+ #---------------------[ Final Parses ]----------------------
+
+ # The target-specific special char escapes for body lines
+ line = doEscape(target,line)
+
+ line = add_inline_tags(line)
+ line = MASK.undo(line)
+
+
+ #---------------------[ Hold or Return? ]-------------------
+
+ ### Now we must choose where to put the parsed line
+ #
+ if not results_box:
+ # List item extra lines
+ if BLOCK.block().endswith('list'):
+ results_box = BLOCK.holdaddsub
+ # Other blocks
+ elif BLOCK.block():
+ results_box = BLOCK.holdadd
+ # No blocks
+ else:
+ line = doFinalEscape(target, line)
+ results_box = ret.append
+
+ results_box(line)
+
+ # EOF: close any open para/verb/lists/table/quotes
+ Debug('EOF',7)
+ while BLOCK.block():
+ ret.extend(BLOCK.blockout())
+
+ # Maybe close some opened title area?
+ if rules['titleblocks']:
+ ret.extend(TITLE.close_all())
+
+ # Maybe a major tag to enclose body? (like DIV for CSS)
+ if TAGS['bodyOpen' ]: ret.insert(0, TAGS['bodyOpen'])
+ if TAGS['bodyClose']: ret.append(TAGS['bodyClose'])
+
+ if CONF['toc-only']: ret = []
+ marked_toc = TITLE.dump_marked_toc(CONF['toc-level'])
+
+ # If dump-source, all parsing is ignored
+ if CONF['dump-source']: ret = dump_source[:]
+
+ return ret, marked_toc
+
+
+
+##############################################################################
+################################### GUI ######################################
+##############################################################################
+#
+# Tk help: http://python.org/topics/tkinter/
+# Tuto: http://ibiblio.org/obp/py4fun/gui/tkPhone.html
+# /usr/lib/python*/lib-tk/Tkinter.py
+#
+# grid table : row=0, column=0, columnspan=2, rowspan=2
+# grid align : sticky='n,s,e,w' (North, South, East, West)
+# pack place : side='top,bottom,right,left'
+# pack fill : fill='x,y,both,none', expand=1
+# pack align : anchor='n,s,e,w' (North, South, East, West)
+# padding : padx=10, pady=10, ipadx=10, ipady=10 (internal)
+# checkbox : offvalue is return if the _user_ deselected the box
+# label align: justify=left,right,center
+
+def load_GUI_resources():
+ "Load all extra modules and methods used by GUI"
+ global askopenfilename, showinfo, showwarning, showerror, tkinter
+ from tkinter.filedialog import askopenfilename
+ from tkinter.messagebox import showinfo,showwarning,showerror
+ import tkinter
+
+class Gui:
+ "Graphical Tk Interface"
+ def __init__(self, conf={}):
+ self.root = tkinter.Tk() # mother window, come to butthead
+ self.root.title(my_name) # window title bar text
+ self.window = self.root # variable "focus" for inclusion
+ self.row = 0 # row count for grid()
+
+ self.action_length = 150 # left column length (pixel)
+ self.frame_margin = 10 # frame margin size (pixel)
+ self.frame_border = 6 # frame border size (pixel)
+
+ # The default Gui colors, can be changed by %!guicolors
+ self.dft_gui_colors = ['#6c6','white','#cf9','#030']
+ self.gui_colors = []
+ self.bg1 = self.fg1 = self.bg2 = self.fg2 = ''
+
+ # On Tk, vars need to be set/get using setvar()/get()
+ self.infile = self.setvar('')
+ self.target = self.setvar('')
+ self.target_name = self.setvar('')
+
+ # The checks appearance order
+ self.checks = [
+ 'headers', 'enum-title', 'toc', 'mask-email', 'toc-only', 'stdout'
+ ]
+
+ # Creating variables for all checks
+ for check in self.checks:
+ setattr(self, 'f_'+check, self.setvar(''))
+
+ # Load RC config
+ self.conf = {}
+ if conf: self.load_config(conf)
+
+ def load_config(self, conf):
+ self.conf = conf
+ self.gui_colors = conf.get('guicolors') or self.dft_gui_colors
+ self.bg1, self.fg1, self.bg2, self.fg2 = self.gui_colors
+ self.root.config(bd=15,bg=self.bg1)
+
+ ### Config as dic for python 1.5 compat (**opts don't work :( )
+ def entry(self, **opts): return tkinter.Entry(self.window, opts)
+ def label(self, txt='', bg=None, **opts):
+ opts.update({'text':txt,'bg':bg or self.bg1})
+ return tkinter.Label(self.window, opts)
+ def button(self,name,cmd,**opts):
+ opts.update({'text':name,'command':cmd})
+ return tkinter.Button(self.window, opts)
+ def check(self,name,checked=0,**opts):
+ bg, fg = self.bg2, self.fg2
+ opts.update({
+ 'text':name,
+ 'onvalue':1,
+ 'offvalue':0,
+ 'activeforeground':fg,
+ 'activebackground':bg,
+ 'highlightbackground':bg,
+ 'fg':fg,
+ 'bg':bg,
+ 'anchor':'w'
+ })
+ chk = tkinter.Checkbutton(self.window, opts)
+ if checked: chk.select()
+ chk.grid(columnspan=2, sticky='w', padx=0)
+ def menu(self,sel,items):
+ return tkinter.OptionMenu(*(self.window,sel)+tuple(items))
+
+ # Handy auxiliary functions
+ def action(self, txt):
+ self.label(
+ txt,
+ fg=self.fg1,
+ bg=self.bg1,
+ wraplength=self.action_length).grid(column=0,row=self.row)
+ def frame_open(self):
+ self.window = tkinter.Frame(
+ self.root,
+ bg=self.bg2,
+ borderwidth=self.frame_border)
+ def frame_close(self):
+ self.window.grid(
+ column=1,
+ row=self.row,
+ sticky='w',
+ padx=self.frame_margin)
+ self.window = self.root
+ self.label('').grid()
+ self.row += 2 # update row count
+ def target_name2key(self):
+ name = self.target_name.get()
+ target = [x for x in TARGETS if TARGET_NAMES[x] == name]
+ try : key = target[0]
+ except: key = ''
+ self.target = self.setvar(key)
+ def target_key2name(self):
+ key = self.target.get()
+ name = TARGET_NAMES.get(key) or key
+ self.target_name = self.setvar(name)
+
+ def exit(self): self.root.destroy()
+ def setvar(self, val): z = tkinter.StringVar() ; z.set(val) ; return z
+
+ def askfile(self):
+ ftypes= [(_('txt2tags files'), ('*.t2t','*.txt')), (_('All files'),'*')]
+ newfile = askopenfilename(filetypes=ftypes)
+ if newfile:
+ self.infile.set(newfile)
+ newconf = process_source_file(newfile)[0]
+ newconf = ConfigMaster().sanity(newconf, gui=1)
+ # Restate all checkboxes after file selection
+ #TODO how to make a refresh without killing it?
+ self.root.destroy()
+ self.__init__(newconf)
+ self.mainwindow()
+
+ def scrollwindow(self, txt='no text!', title=''):
+ # Create components
+ win = tkinter.Toplevel() ; win.title(title)
+ frame = tkinter.Frame(win)
+ scroll = tkinter.Scrollbar(frame)
+ text = tkinter.Text(frame,yscrollcommand=scroll.set)
+ button = tkinter.Button(win)
+ # Config
+ text.insert(tkinter.END, '\n'.join(txt))
+ scroll.config(command=text.yview)
+ button.config(text=_('Close'), command=win.destroy)
+ button.focus_set()
+ # Packing
+ text.pack(side='left', fill='both', expand=1)
+ scroll.pack(side='right', fill='y')
+ frame.pack(fill='both', expand=1)
+ button.pack(ipadx=30)
+
+ def runprogram(self):
+ global CMDLINE_RAW
+ # Prepare
+ self.target_name2key()
+ infile, target = self.infile.get(), self.target.get()
+ # Sanity
+ if not target:
+ showwarning(my_name,_("You must select a target type!"))
+ return
+ if not infile:
+ showwarning(my_name,_("You must provide the source file location!"))
+ return
+ # Compose cmdline
+ guiflags = []
+ real_cmdline_conf = ConfigMaster(CMDLINE_RAW).parse()
+ if 'infile' in real_cmdline_conf:
+ del real_cmdline_conf['infile']
+ if 'target' in real_cmdline_conf:
+ del real_cmdline_conf['target']
+ real_cmdline = CommandLine().compose_cmdline(real_cmdline_conf)
+ default_outfile = ConfigMaster().get_outfile_name(
+ {'sourcefile':infile, 'outfile':'', 'target':target})
+ for opt in self.checks:
+ val = int(getattr(self, 'f_%s'%opt).get() or "0")
+ if opt == 'stdout': opt = 'outfile'
+ on_config = self.conf.get(opt) or 0
+ on_cmdline = real_cmdline_conf.get(opt) or 0
+ if opt == 'outfile':
+ if on_config == STDOUT: on_config = 1
+ else: on_config = 0
+ if on_cmdline == STDOUT: on_cmdline = 1
+ else: on_cmdline = 0
+ if val != on_config or (
+ val == on_config == on_cmdline and
+ opt in real_cmdline_conf):
+ if val:
+ # Was not set, but user selected on GUI
+ Debug("user turned ON: %s"%opt)
+ if opt == 'outfile': opt = '-o-'
+ else: opt = '--%s'%opt
+ else:
+ # Was set, but user deselected on GUI
+ Debug("user turned OFF: %s"%opt)
+ if opt == 'outfile':
+ opt = "-o%s"%default_outfile
+ else: opt = '--no-%s'%opt
+ guiflags.append(opt)
+ cmdline = [my_name, '-t', target] + real_cmdline + guiflags + [infile]
+ Debug('Gui/Tk cmdline: %s' % cmdline, 5)
+ # Run!
+ cmdline_raw_orig = CMDLINE_RAW
+ try:
+ # Fake the GUI cmdline as the real one, and parse file
+ CMDLINE_RAW = CommandLine().get_raw_config(cmdline[1:])
+ data = process_source_file(infile)
+ # On GUI, convert_* returns the data, not finish_him()
+ outlist, config = convert_this_files([data])
+ # On GUI and STDOUT, finish_him() returns the data
+ result = finish_him(outlist, config)
+ # Show outlist in s a nice new window
+ if result:
+ outlist, config = result
+ title = _('%s: %s converted to %s') % (
+ my_name,
+ os.path.basename(infile),
+ config['target'].upper())
+ self.scrollwindow(outlist, title)
+ # Show the "file saved" message
+ else:
+ msg = "%s\n\n %s\n%s\n\n %s\n%s"%(
+ _('Conversion done!'),
+ _('FROM:'), infile,
+ _('TO:'), config['outfile'])
+ showinfo(my_name, msg)
+ except error: # common error (windowed), not quit
+ pass
+ except: # fatal error (windowed and printed)
+ errormsg = getUnknownErrorMessage()
+ print(errormsg)
+ showerror(_('%s FATAL ERROR!')%my_name,errormsg)
+ self.exit()
+ CMDLINE_RAW = cmdline_raw_orig
+
+ def mainwindow(self):
+ self.infile.set(self.conf.get('sourcefile') or '')
+ self.target.set(self.conf.get('target') or _('-- select one --'))
+ outfile = self.conf.get('outfile')
+ if outfile == STDOUT: # map -o-
+ self.conf['stdout'] = 1
+ if self.conf.get('headers') == None:
+ self.conf['headers'] = 1 # map default
+
+ action1 = _("Enter the source file location:")
+ action2 = _("Choose the target document type:")
+ action3 = _("Some options you may check:")
+ action4 = _("Some extra options:")
+ checks_txt = {
+ 'headers' : _("Include headers on output"),
+ 'enum-title': _("Number titles (1, 1.1, 1.1.1, etc)"),
+ 'toc' : _("Do TOC also (Table of Contents)"),
+ 'mask-email': _("Hide e-mails from SPAM robots"),
+
+ 'toc-only' : _("Just do TOC, nothing more"),
+ 'stdout' : _("Dump to screen (Don't save target file)")
+ }
+ targets_menu = [TARGET_NAMES[x] for x in TARGETS]
+
+ # Header
+ self.label("%s %s"%(my_name.upper(), my_version),
+ bg=self.bg2, fg=self.fg2).grid(columnspan=2, ipadx=10)
+ self.label(_("ONE source, MULTI targets")+'\n%s\n'%my_url,
+ bg=self.bg1, fg=self.fg1).grid(columnspan=2)
+ self.row = 2
+ # Choose input file
+ self.action(action1) ; self.frame_open()
+ e_infile = self.entry(textvariable=self.infile,width=25)
+ e_infile.grid(row=self.row, column=0, sticky='e')
+ if not self.infile.get(): e_infile.focus_set()
+ self.button(_("Browse"), self.askfile).grid(
+ row=self.row, column=1, sticky='w', padx=10)
+ # Show outfile name, style and encoding (if any)
+ txt = ''
+ if outfile:
+ txt = outfile
+ if outfile == STDOUT: txt = _('')
+ l_output = self.label(_('Output: ')+txt, fg=self.fg2, bg=self.bg2)
+ l_output.grid(columnspan=2, sticky='w')
+ for setting in ['style','encoding']:
+ if self.conf.get(setting):
+ name = setting.capitalize()
+ val = self.conf[setting]
+ self.label('%s: %s'%(name, val),
+ fg=self.fg2, bg=self.bg2).grid(
+ columnspan=2, sticky='w')
+ # Choose target
+ self.frame_close() ; self.action(action2)
+ self.frame_open()
+ self.target_key2name()
+ self.menu(self.target_name, targets_menu).grid(
+ columnspan=2, sticky='w')
+ # Options checkboxes label
+ self.frame_close() ; self.action(action3)
+ self.frame_open()
+ # Compose options check boxes, example:
+ # self.check(checks_txt['toc'],1,variable=self.f_toc)
+ for check in self.checks:
+ # Extra options label
+ if check == 'toc-only':
+ self.frame_close() ; self.action(action4)
+ self.frame_open()
+ txt = checks_txt[check]
+ var = getattr(self, 'f_'+check)
+ checked = self.conf.get(check)
+ self.check(txt,checked,variable=var)
+ self.frame_close()
+ # Spacer and buttons
+ self.label('').grid() ; self.row += 1
+ b_quit = self.button(_("Quit"), self.exit)
+ b_quit.grid(row=self.row, column=0, sticky='w', padx=30)
+ b_conv = self.button(_("Convert!"), self.runprogram)
+ b_conv.grid(row=self.row, column=1, sticky='e', padx=30)
+ if self.target.get() and self.infile.get():
+ b_conv.focus_set()
+
+ # As documentation told me
+ if sys.platform.startswith('win'):
+ self.root.iconify()
+ self.root.update()
+ self.root.deiconify()
+
+ self.root.mainloop()
+
+
+##############################################################################
+##############################################################################
+
+def exec_command_line(user_cmdline=[]):
+ global CMDLINE_RAW, RC_RAW, DEBUG, VERBOSE, QUIET, GUI, Error
+
+ # Extract command line data
+ cmdline_data = user_cmdline or sys.argv[1:]
+ CMDLINE_RAW = CommandLine().get_raw_config(cmdline_data, relative=1)
+ cmdline_parsed = ConfigMaster(CMDLINE_RAW).parse()
+ DEBUG = cmdline_parsed.get('debug' ) or 0
+ VERBOSE = cmdline_parsed.get('verbose') or 0
+ QUIET = cmdline_parsed.get('quiet' ) or 0
+ GUI = cmdline_parsed.get('gui' ) or 0
+ infiles = cmdline_parsed.get('infile' ) or []
+
+ Message(_("Txt2tags %s processing begins")%my_version,1)
+
+ # The easy ones
+ if cmdline_parsed.get('help' ): Quit(USAGE)
+ if cmdline_parsed.get('version'): Quit(VERSIONSTR)
+ if cmdline_parsed.get('targets'):
+ listTargets()
+ Quit()
+
+ # Multifile haters
+ if len(infiles) > 1:
+ errmsg=_("Option --%s can't be used with multiple input files")
+ for option in NO_MULTI_INPUT:
+ if cmdline_parsed.get(option):
+ Error(errmsg%option)
+
+ Debug("system platform: %s"%sys.platform)
+ Debug("python version: %s"%(sys.version.split('(')[0]))
+ Debug("line break char: %s"%repr(LB))
+ Debug("command line: %s"%sys.argv)
+ Debug("command line raw config: %s"%CMDLINE_RAW,1)
+
+ # Extract RC file config
+ if cmdline_parsed.get('rc') == 0:
+ Message(_("Ignoring user configuration file"),1)
+ else:
+ rc_file = get_rc_path()
+ if os.path.isfile(rc_file):
+ Message(_("Loading user configuration file"),1)
+ RC_RAW = ConfigLines(file_=rc_file).get_raw_config()
+
+ Debug("rc file: %s"%rc_file)
+ Debug("rc file raw config: %s"%RC_RAW,1)
+
+ # Get all infiles config (if any)
+ infiles_config = get_infiles_config(infiles)
+
+ # Is GUI available?
+ # Try to load and start GUI interface for --gui
+ if GUI:
+ try:
+ load_GUI_resources()
+ Debug("GUI resources OK (Tk module is installed)")
+ winbox = Gui()
+ Debug("GUI display OK")
+ GUI = 1
+ except:
+ Debug("GUI Error: no Tk module or no DISPLAY")
+ GUI = 0
+
+ # User forced --gui, but it's not available
+ if cmdline_parsed.get('gui') and not GUI:
+ print(getTraceback()); print()
+ Error(
+ "Sorry, I can't run my Graphical Interface - GUI\n"
+ "- Check if Python Tcl/Tk module is installed (Tkinter)\n"
+ "- Make sure you are in a graphical environment (like X)")
+
+ # Okay, we will use GUI
+ if GUI:
+ Message(_("We are on GUI interface"),1)
+
+ # Redefine Error function to raise exception instead sys.exit()
+ def Error(msg):
+ showerror(_('txt2tags ERROR!'), msg)
+ raise error
+
+ # If no input file, get RC+cmdline config, else full config
+ if not infiles:
+ gui_conf = ConfigMaster(RC_RAW+CMDLINE_RAW).parse()
+ else:
+ try : gui_conf = infiles_config[0][0]
+ except: gui_conf = {}
+
+ # Sanity is needed to set outfile and other things
+ gui_conf = ConfigMaster().sanity(gui_conf, gui=1)
+ Debug("GUI config: %s"%gui_conf,5)
+
+ # Insert config and populate the nice window!
+ winbox.load_config(gui_conf)
+ winbox.mainwindow()
+
+ # Console mode rocks forever!
+ else:
+ Message(_("We are on Command Line interface"),1)
+
+ # Called with no arguments, show error
+ # TODO#1: this checking should be only in ConfigMaster.sanity()
+ if not infiles:
+ Error(_('Missing input file (try --help)') + '\n\n' +
+ _('Please inform an input file (.t2t) at the end of the command.') + '\n' +
+ _('Example:') + ' %s -t html %s' % (my_name, _('file.t2t')))
+
+ convert_this_files(infiles_config)
+
+ Message(_("Txt2tags finished successfully"),1)
+
+if __name__ == '__main__':
+ try:
+ exec_command_line()
+ except error as msg:
+ sys.stderr.write("%s\n"%msg)
+ sys.stderr.flush()
+ sys.exit(1)
+ except SystemExit:
+ pass
+ except:
+ sys.stderr.write(getUnknownErrorMessage())
+ sys.stderr.flush()
+ sys.exit(1)
+ Quit()
+
+# The End.
diff --git a/src/exporter/__init__.py b/src/exporter/__init__.py
new file mode 100644
index 0000000..cf9be85
--- /dev/null
+++ b/src/exporter/__init__.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+#--!-- coding: utf8 --!--
+
+import collections
+from qt import *
+from .html import htmlExporter
+from .arbo import arboExporter
+from .odt import odtExporter
+
+formats = collections.OrderedDict([
+ #Format
+ # Readable name
+ # Class
+ # QFileDialog filter
+ ('html', (
+ qApp.translate("exporter", "HTML"),
+ htmlExporter,
+ qApp.translate("exporter", "HTML Document (*.html)"))),
+ ('arbo', (
+ qApp.translate("exporter", "Arborescence"),
+ arboExporter,
+ None)),
+ ('odt', (
+ qApp.translate("exporter", "OpenDocument (LibreOffice)"),
+ odtExporter,
+ qApp.translate("exporter", "OpenDocument (*.odt)"))),
+ ('epub', (
+ "ePub (not yet)",
+ None,
+ None)),
+])
\ No newline at end of file
diff --git a/src/exporter/arbo.py b/src/exporter/arbo.py
new file mode 100644
index 0000000..2664eeb
--- /dev/null
+++ b/src/exporter/arbo.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+#--!-- coding: utf8 --!--
+
+from qt import *
+from enums import *
+from functions import *
+
+class arboExporter():
+
+ requires = ["path"]
+
+ def __init__(self):
+ pass
+
+ def doCompile(self, path):
+ #FIXME: overwrites when items have identical names
+ mw = mainWindow()
+ root = mw.mdlOutline.rootItem
+
+ def writeItem(item, path):
+ if item.isFolder():
+ path2 = os.path.join(path, item.title())
+
+ try:
+ os.mkdir(path2)
+ except FileExistsError:
+ pass
+
+ for c in item.children():
+ writeItem(c, path2)
+
+ else:
+ ext = ".t2t" if item.isT2T() else \
+ ".html" if item.isHTML() else \
+ ".txt"
+ path2 = os.path.join(path, item.title() + ext)
+ f = open(path2, "w")
+ text = self.formatText(item.text(), item.type())
+ f.write(text)
+
+ for c in root.children():
+ writeItem(c, path)
+
+
+ def formatText(self, text, _type):
+ if _type == "t2t":
+ # Empty lines for headers
+ text = "\n\n\n" + text
+
+ return text
+
+
+
diff --git a/src/exporter/basic.py b/src/exporter/basic.py
new file mode 100644
index 0000000..f6a4db1
--- /dev/null
+++ b/src/exporter/basic.py
@@ -0,0 +1,26 @@
+#!/usr/bin/env python
+#--!-- coding: utf8 --!--
+
+from qt import *
+from enums import *
+from functions import *
+import subprocess
+
+class basicExporter():
+
+ def __init__(self):
+ pass
+
+ def runT2T(self, text, target="html"):
+
+ cmdl = ['txt2tags', '-t', target, '--enc=utf-8', '--no-headers', '-o', '-', '-']
+
+ cmd = subprocess.Popen(('echo', text), stdout=subprocess.PIPE)
+ try:
+ output = subprocess.check_output(cmdl, stdin=cmd.stdout, stderr=subprocess.STDOUT) # , cwd="/tmp"
+ except subprocess.CalledProcessError as e:
+ print("Error!")
+ return text
+ cmd.wait()
+
+ return output.decode("utf-8")
diff --git a/src/exporter/html.py b/src/exporter/html.py
new file mode 100644
index 0000000..e95b782
--- /dev/null
+++ b/src/exporter/html.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+#--!-- coding: utf8 --!--
+
+from qt import *
+from enums import *
+from functions import *
+from exporter.basic import basicExporter
+import subprocess
+
+class htmlExporter(basicExporter):
+
+ requires = ["filename"]
+
+ def __init__(self):
+ pass
+
+ def doCompile(self, filename):
+ mw = mainWindow()
+ root = mw.mdlOutline.rootItem
+
+ html = ""
+
+ def appendItem(item):
+ if item.isFolder():
+ html = ""
+ title = "{t}\n".format(
+ l = str(item.level() + 1),
+ t = item.title())
+ html += title
+
+ for c in item.children():
+ html += appendItem(c)
+
+ return html
+
+ else:
+ text = self.formatText(item.text(), item.type())
+ return text
+
+ for c in root.children():
+ html += appendItem(c)
+
+
+ template = """
+
+
+
+ {TITLE}
+
+
+ {BODY}
+
+"""
+
+ f = open(filename, "w")
+ f.write(template.format(
+ TITLE="FIXME",
+ BODY=html))
+
+ def formatText(self, text, _type):
+
+ if not text:
+ return text
+
+ if _type == "t2t":
+ text = self.runT2T(text)
+
+ elif _type == "txt":
+ text = text.replace("\n", "
")
+
+ return text + "
"
+
+ def runT2T(self, text, target="html"):
+
+ cmdl = ['txt2tags', '-t', target, '--enc=utf-8', '--no-headers', '-o', '-', '-']
+
+ cmd = subprocess.Popen(('echo', text), stdout=subprocess.PIPE)
+ try:
+ output = subprocess.check_output(cmdl, stdin=cmd.stdout, stderr=subprocess.STDOUT) # , cwd="/tmp"
+ except subprocess.CalledProcessError as e:
+ print("Error!")
+ return text
+ cmd.wait()
+
+ return output.decode("utf-8")
diff --git a/src/exporter/odt.py b/src/exporter/odt.py
new file mode 100644
index 0000000..2e4b1f0
--- /dev/null
+++ b/src/exporter/odt.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+#--!-- coding: utf8 --!--
+
+from qt import *
+from enums import *
+from functions import *
+from exporter.basic import basicExporter
+
+class odtExporter(basicExporter):
+
+ requires = ["filename"]
+
+ def __init__(self):
+ pass
+
+ def doCompile(self, filename):
+ mw = mainWindow()
+ root = mw.mdlOutline.rootItem
+
+ doc = QTextDocument()
+ cursor = QTextCursor(doc)
+
+
+ def appendItem(item):
+ if item.isFolder():
+
+ cursor.setPosition(doc.characterCount() - 1)
+ title = "{t}
\n".format(
+ l = str(item.level() + 1),
+ t = item.title())
+ cursor.insertHtml(title)
+
+ for c in item.children():
+ appendItem(c)
+
+ else:
+ text = self.formatText(item.text(), item.type())
+ cursor.setPosition(doc.characterCount() - 1)
+ cursor.insertHtml(text)
+
+ for c in root.children():
+ appendItem(c)
+
+ dw = QTextDocumentWriter(filename, "odt")
+ dw.write(doc)
+
+ def formatText(self, text, _type):
+
+ if not text:
+ return text
+
+ if _type == "t2t":
+ text = self.runT2T(text)
+
+ elif _type == "txt":
+ text = text.replace("\n", "
")
+
+ return text + "
"
diff --git a/src/mainWindow.py b/src/mainWindow.py
index d18c66f..adec764 100644
--- a/src/mainWindow.py
+++ b/src/mainWindow.py
@@ -5,6 +5,7 @@ from qt import *
from ui.mainWindow import *
from ui.helpLabel import helpLabel
+from ui.compileDialog import compileDialog
from loadSave import *
from enums import *
from models.outlineModel import *
@@ -82,6 +83,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.actOpen.triggered.connect(self.welcome.openFile)
self.actSave.triggered.connect(self.saveDatas)
self.actSaveAs.triggered.connect(self.welcome.saveAsFile)
+ self.actCompile.triggered.connect(self.doCompile)
self.actLabels.triggered.connect(self.settingsLabel)
self.actStatus.triggered.connect(self.settingsStatus)
self.actSettings.triggered.connect(self.settingsWindow)
@@ -1070,4 +1072,13 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.mainEditor.updateTreeView()
self.treePlanOutline.viewport().update()
if item == "Tree":
- self.treeRedacOutline.viewport().update()
\ No newline at end of file
+ self.treeRedacOutline.viewport().update()
+
+
+###############################################################################
+# COMPILE
+###############################################################################
+
+ def doCompile(self):
+ self.compileDialog = compileDialog()
+ self.compileDialog.show()
\ No newline at end of file
diff --git a/src/models/outlineModel.py b/src/models/outlineModel.py
index 0d2ef54..e3b848e 100644
--- a/src/models/outlineModel.py
+++ b/src/models/outlineModel.py
@@ -619,6 +619,9 @@ class outlineItem():
def isText(self):
return self._data[Outline.type] == "txt"
+ def text(self):
+ return self.data(Outline.text.value)
+
def compile(self):
if self._data[Outline.compile] in ["0", 0]:
return False
diff --git a/src/ui/compileDialog.py b/src/ui/compileDialog.py
new file mode 100644
index 0000000..87e19c7
--- /dev/null
+++ b/src/ui/compileDialog.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python
+#--!-- coding: utf8 --!--
+
+from qt import *
+from enums import *
+from models.outlineModel import *
+from ui.compileDialog_ui import *
+from functions import *
+import os
+import exporter
+
+class compileDialog(QDialog, Ui_compileDialog):
+
+ def __init__(self, parent=None):
+ QDialog.__init__(self, parent)
+ self.setupUi(self)
+
+ self.btnPath.clicked.connect(self.getPath)
+ self.btnFilename.clicked.connect(self.getFilename)
+
+ self.btnCompile.clicked.connect(self.doCompile)
+ self.cmbTargets.activated.connect(self.updateUI)
+
+ self.txtPath.setText("/home/olivier/Documents/Travail/Geekeries/Python/manuskript/ExportTest")
+ self.txtFilename.setText("/home/olivier/Documents/Travail/Geekeries/Python/manuskript/ExportTest/test.html")
+
+
+ self.populatesTarget()
+ self.updateUI()
+
+###############################################################################
+# UI
+###############################################################################
+
+ def populatesTarget(self):
+ for code in exporter.formats:
+ self.cmbTargets.addItem(exporter.formats[code][0], code)
+
+ def updateUI(self):
+ target = self.cmbTargets.currentData()
+
+ if not exporter.formats[target][1]:
+ self.btnCompile.setEnabled(False)
+ requires = []
+ else:
+ self.btnCompile.setEnabled(True)
+ requires = exporter.formats[target][1].requires
+
+ self.wPath.setVisible("path" in requires)
+ self.wFilename.setVisible("filename" in requires)
+
+ def startWorking(self):
+ # Setting override cursor
+ qApp.setOverrideCursor(Qt.WaitCursor)
+
+ # Button
+ self.btnCompile.setEnabled(False)
+ self.txtBtn = self.btnCompile.text()
+ self.btnCompile.setText(self.tr("Working..."))
+
+ def stopWorking(self):
+ # Removing override cursor
+ qApp.restoreOverrideCursor()
+
+ # Button
+ self.btnCompile.setEnabled(True)
+ self.btnCompile.setText(self.txtBtn)
+
+###############################################################################
+# USER INPUTS
+###############################################################################
+
+ def getPath(self):
+ path = self.txtPath.text()
+ path = QFileDialog.getExistingDirectory(self, self.tr("Chose export folder"), path)
+ if path:
+ self.txtPath.setText(path)
+
+ def getFilename(self):
+ fn = self.txtFilename.text()
+ target = self.cmbTargets.currentData()
+ fltr = exporter.formats[target][2]
+ fn = QFileDialog.getSaveFileName(self, self.tr("Chose export target"), fn, fltr)
+
+ if fn[0]:
+ self.txtFilename.setText(fn[0])
+
+
+###############################################################################
+# COMPILE
+###############################################################################
+
+ def doCompile(self):
+ target = self.cmbTargets.currentData()
+
+ self.startWorking()
+
+ if target == "arbo":
+ compiler = exporter.formats[target][1]()
+ compiler.doCompile(self.txtPath.text())
+
+ elif target in ["html", "odt"]:
+ compiler = exporter.formats[target][1]()
+ compiler.doCompile(self.txtFilename.text())
+
+ self.stopWorking()
+
\ No newline at end of file
diff --git a/src/ui/compileDialog_ui.py b/src/ui/compileDialog_ui.py
new file mode 100644
index 0000000..8984c83
--- /dev/null
+++ b/src/ui/compileDialog_ui.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'src/ui/compileDialog_ui.ui'
+#
+# Created by: PyQt5 UI code generator 5.4.1
+#
+# WARNING! All changes made in this file will be lost!
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+class Ui_compileDialog(object):
+ def setupUi(self, compileDialog):
+ compileDialog.setObjectName("compileDialog")
+ compileDialog.resize(627, 343)
+ compileDialog.setModal(True)
+ self.verticalLayout = QtWidgets.QVBoxLayout(compileDialog)
+ self.verticalLayout.setObjectName("verticalLayout")
+ self.horizontalLayout = QtWidgets.QHBoxLayout()
+ self.horizontalLayout.setObjectName("horizontalLayout")
+ spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout.addItem(spacerItem)
+ self.label = QtWidgets.QLabel(compileDialog)
+ self.label.setObjectName("label")
+ self.horizontalLayout.addWidget(self.label)
+ self.cmbTargets = QtWidgets.QComboBox(compileDialog)
+ self.cmbTargets.setObjectName("cmbTargets")
+ self.horizontalLayout.addWidget(self.cmbTargets)
+ spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout.addItem(spacerItem1)
+ self.verticalLayout.addLayout(self.horizontalLayout)
+ self.wPath = QtWidgets.QWidget(compileDialog)
+ self.wPath.setObjectName("wPath")
+ self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.wPath)
+ self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0)
+ self.horizontalLayout_3.setObjectName("horizontalLayout_3")
+ self.label_2 = QtWidgets.QLabel(self.wPath)
+ self.label_2.setObjectName("label_2")
+ self.horizontalLayout_3.addWidget(self.label_2)
+ self.txtPath = QtWidgets.QLineEdit(self.wPath)
+ self.txtPath.setObjectName("txtPath")
+ self.horizontalLayout_3.addWidget(self.txtPath)
+ self.btnPath = QtWidgets.QPushButton(self.wPath)
+ self.btnPath.setObjectName("btnPath")
+ self.horizontalLayout_3.addWidget(self.btnPath)
+ self.verticalLayout.addWidget(self.wPath)
+ self.wFilename = QtWidgets.QWidget(compileDialog)
+ self.wFilename.setObjectName("wFilename")
+ self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.wFilename)
+ self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
+ self.horizontalLayout_4.setObjectName("horizontalLayout_4")
+ self.label_3 = QtWidgets.QLabel(self.wFilename)
+ self.label_3.setObjectName("label_3")
+ self.horizontalLayout_4.addWidget(self.label_3)
+ self.txtFilename = QtWidgets.QLineEdit(self.wFilename)
+ self.txtFilename.setObjectName("txtFilename")
+ self.horizontalLayout_4.addWidget(self.txtFilename)
+ self.btnFilename = QtWidgets.QPushButton(self.wFilename)
+ self.btnFilename.setObjectName("btnFilename")
+ self.horizontalLayout_4.addWidget(self.btnFilename)
+ self.verticalLayout.addWidget(self.wFilename)
+ spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
+ self.verticalLayout.addItem(spacerItem2)
+ self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_2.setObjectName("horizontalLayout_2")
+ spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout_2.addItem(spacerItem3)
+ self.btnCompile = QtWidgets.QPushButton(compileDialog)
+ self.btnCompile.setObjectName("btnCompile")
+ self.horizontalLayout_2.addWidget(self.btnCompile)
+ self.btnCancel = QtWidgets.QPushButton(compileDialog)
+ self.btnCancel.setObjectName("btnCancel")
+ self.horizontalLayout_2.addWidget(self.btnCancel)
+ self.verticalLayout.addLayout(self.horizontalLayout_2)
+
+ self.retranslateUi(compileDialog)
+ QtCore.QMetaObject.connectSlotsByName(compileDialog)
+
+ def retranslateUi(self, compileDialog):
+ _translate = QtCore.QCoreApplication.translate
+ compileDialog.setWindowTitle(_translate("compileDialog", "Dialog"))
+ self.label.setText(_translate("compileDialog", "Compile as:"))
+ self.label_2.setText(_translate("compileDialog", "Folder:"))
+ self.btnPath.setText(_translate("compileDialog", "..."))
+ self.label_3.setText(_translate("compileDialog", "File name:"))
+ self.btnFilename.setText(_translate("compileDialog", "..."))
+ self.btnCompile.setText(_translate("compileDialog", "Compile"))
+ self.btnCancel.setText(_translate("compileDialog", "Cancel"))
+
diff --git a/src/ui/compileDialog_ui.ui b/src/ui/compileDialog_ui.ui
new file mode 100644
index 0000000..37edc63
--- /dev/null
+++ b/src/ui/compileDialog_ui.ui
@@ -0,0 +1,154 @@
+
+
+ compileDialog
+
+
+
+ 0
+ 0
+ 627
+ 343
+
+
+
+ Dialog
+
+
+ true
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Compile as:
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+
-
+
+
+ Folder:
+
+
+
+ -
+
+
+ -
+
+
+ ...
+
+
+
+
+
+
+ -
+
+
+
-
+
+
+ File name:
+
+
+
+ -
+
+
+ -
+
+
+ ...
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Compile
+
+
+
+ -
+
+
+ Cancel
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ui/mainWindow.py b/src/ui/mainWindow.py
index 60deaac..2bc7fec 100644
--- a/src/ui/mainWindow.py
+++ b/src/ui/mainWindow.py
@@ -1019,12 +1019,16 @@ class Ui_MainWindow(object):
icon = QtGui.QIcon.fromTheme("window-close")
self.actCloseProject.setIcon(icon)
self.actCloseProject.setObjectName("actCloseProject")
+ self.actCompile = QtWidgets.QAction(MainWindow)
+ self.actCompile.setObjectName("actCompile")
self.menuFile.addAction(self.actOpen)
self.menuFile.addAction(self.menuRecents.menuAction())
self.menuFile.addAction(self.actSave)
self.menuFile.addAction(self.actSaveAs)
self.menuFile.addAction(self.actCloseProject)
self.menuFile.addSeparator()
+ self.menuFile.addAction(self.actCompile)
+ self.menuFile.addSeparator()
self.menuFile.addAction(self.actQuit)
self.menuMode.addAction(self.actModeNorma)
self.menuMode.addAction(self.actModeSimple)
@@ -1168,18 +1172,20 @@ class Ui_MainWindow(object):
self.actSettings.setText(_translate("MainWindow", "Settings"))
self.actSettings.setShortcut(_translate("MainWindow", "F8"))
self.actCloseProject.setText(_translate("MainWindow", "Close project"))
+ self.actCompile.setText(_translate("MainWindow", "Compile"))
+ self.actCompile.setShortcut(_translate("MainWindow", "F6"))
-from ui.editors.mainEditor import mainEditor
-from ui.views.metadataView import metadataView
+from ui.views.persoTreeView import persoTreeView
+from ui.cheatSheet import cheatSheet
from ui.search import search
from ui.views.outlineView import outlineView
-from ui.collapsibleGroupBox2 import collapsibleGroupBox2
-from ui.views.persoTreeView import persoTreeView
-from ui.sldImportance import sldImportance
-from ui.views.basicItemView import basicItemView
-from ui.views.treeView import treeView
-from ui.cheatSheet import cheatSheet
-from ui.views.plotTreeView import plotTreeView
-from ui.welcome import welcome
-from ui.views.textEditView import textEditView
from ui.views.lineEditView import lineEditView
+from ui.sldImportance import sldImportance
+from ui.views.plotTreeView import plotTreeView
+from ui.editors.mainEditor import mainEditor
+from ui.collapsibleGroupBox2 import collapsibleGroupBox2
+from ui.welcome import welcome
+from ui.views.basicItemView import basicItemView
+from ui.views.metadataView import metadataView
+from ui.views.treeView import treeView
+from ui.views.textEditView import textEditView
diff --git a/src/ui/mainWindow.ui b/src/ui/mainWindow.ui
index 5e89f52..495e2c7 100644
--- a/src/ui/mainWindow.ui
+++ b/src/ui/mainWindow.ui
@@ -1795,6 +1795,8 @@
+
+