File indexing completed on 2024-12-01 13:47:51

0001 # -*- coding: utf-8 -*-
0002 
0003 """
0004 Report info, warning and error messages.
0005 
0006 Functions for Pology tools to report PO messages to the user at runtime,
0007 in different contexts and scenario. May colorize some output.
0008 
0009 @author: Chusslove Illich (Часлав Илић) <caslav.ilic@gmx.net>
0010 @author: Nick Shaforostoff (Николай Шафоростов) <shaforostoff@kde.ru>
0011 @license: GPLv3
0012 """
0013 
0014 # NOTE: These functions are not in pology.report module,
0015 # as that would cause circular module dependencies.
0016 
0017 from copy import deepcopy
0018 import os
0019 import re
0020 import sys
0021 
0022 from pology import _, n_
0023 from pology.message import Message
0024 from pology.colors import ColorString, cjoin, cinterp
0025 from pology.diff import adapt_spans
0026 from pology.escape import escape_c as escape
0027 from pology.monitored import Monpair
0028 from pology.report import report, warning, error, format_item_list
0029 
0030 
0031 # FIXME: Make this a public function in getfunc module.
0032 _modules_on_request = {}
0033 def _get_module (name, cmsg=None):
0034 
0035     if name not in _modules_on_request:
0036         try:
0037             _modules_on_request[name] = __import__(name)
0038         except:
0039             if cmsg:
0040                 warning(_("@info",
0041                           "Cannot import module '%(mod)s'; consequence:\n"
0042                           "%(msg)s",
0043                           mod=name, msg=cmsg))
0044             else:
0045                 warning(_("@info",
0046                           "Cannot import module '%(mod)s'.",
0047                           mod=name))
0048             _modules_on_request[name] = None
0049 
0050     return _modules_on_request[name]
0051 
0052 
0053 def report_on_msg (text, msg, cat, subsrc=None, file=sys.stdout):
0054     """
0055     Report on a PO message.
0056 
0057     Outputs the message reference (catalog name and message position),
0058     along with the report text.
0059 
0060     @param text: text to report
0061     @type text: string
0062     @param msg: the message for which the text is reported
0063     @type msg: L{Message_base}
0064     @param cat: the catalog where the message lives
0065     @type cat: L{Catalog}
0066     @param subsrc: more detailed source of the message
0067     @type subsrc: C{None} or string
0068     @param file: send output to this file descriptor
0069     @type file: C{file}
0070     """
0071 
0072     posinfo = _msg_pos_fmt(cat.filename, msg.refline, msg.refentry)
0073     text = cinterp("%s: %s", posinfo, text)
0074     report(text, subsrc=subsrc, showcmd=False)
0075 
0076 
0077 def warning_on_msg (text, msg, cat, subsrc=None, file=sys.stderr):
0078     """
0079     Warning on a PO message.
0080 
0081     Outputs the message reference (catalog name and the message position),
0082     along with the warning text.
0083 
0084     @param text: text to report
0085     @type text: string
0086     @param msg: the message for which the text is reported
0087     @type msg: L{Message_base}
0088     @param cat: the catalog where the message lives
0089     @type cat: L{Catalog}
0090     @param subsrc: more detailed source of the message
0091     @type subsrc: C{None} or string
0092     @param file: send output to this file descriptor
0093     @type file: C{file}
0094     """
0095 
0096     posinfo = _msg_pos_fmt(cat.filename, msg.refline, msg.refentry)
0097     text = cinterp("%s: %s", posinfo, text)
0098     warning(text, subsrc=subsrc, showcmd=False)
0099 
0100 
0101 def error_on_msg (text, msg, cat, code=1, subsrc=None, file=sys.stderr):
0102     """
0103     Error on a PO message (aborts the execution).
0104 
0105     Outputs the message reference (catalog name and message position),
0106     along with the error text. Aborts execution with the given code.
0107 
0108     @param text: text to report
0109     @type text: string
0110     @param msg: the message for which the text is reported
0111     @type msg: L{Message_base}
0112     @param cat: the catalog where the message lives
0113     @type cat: L{Catalog}
0114     @param code: the exit code
0115     @type code: int
0116     @param subsrc: more detailed source of the message
0117     @type subsrc: C{None} or string
0118     @param file: send output to this file descriptor
0119     @type file: C{file}
0120     """
0121 
0122     posinfo = _msg_pos_fmt(cat.filename, msg.refline, msg.refentry)
0123     text = cinterp("%s: %s", posinfo, text)
0124     error(text, code=code, subsrc=subsrc, showcmd=True)
0125 
0126 
0127 def report_on_msg_hl (highlight, msg, cat, fmsg=None,
0128                       subsrc=None, file=sys.stdout):
0129     """
0130     Report on parts of a PO message.
0131 
0132     For each of the spans found in the L{highlight<report_msg_content>}
0133     specification which have a note attached, outputs the position reference
0134     (catalog name, message position, spanned segment) and the span note.
0135     The highlight can be relative to a somewhat modified, filtered message
0136     instead of the original one.
0137 
0138     @param highlight: highlight specification
0139     @type highlight: L{highlight<report_msg_content>}
0140     @param msg: the message for which the text is reported
0141     @type msg: L{Message_base}
0142     @param cat: the catalog where the message lives
0143     @type cat: L{Catalog}
0144     @param fmsg: filtered message to which the highlight corresponds
0145     @type fmsg: L{Message_base}
0146     @param subsrc: more detailed source of the message
0147     @type subsrc: C{None} or string
0148     @param file: send output to this file descriptor
0149     @type file: C{file}
0150     """
0151 
0152     refpos = _msg_pos_fmt(cat.filename, msg.refline, msg.refentry)
0153 
0154     if not fmsg: # use original message as filtered if not given
0155         fmsg = msg
0156 
0157     for hspec in highlight:
0158         name, item, spans = hspec[:3]
0159 
0160         if name == "msgctxt":
0161             text = msg.msgctxt or ""
0162             ftext = fmsg.msgctxt or ""
0163         elif name == "msgid":
0164             text = msg.msgid
0165             ftext = fmsg.msgid
0166         elif name == "msgid_plural":
0167             text = msg.msgid_plural or ""
0168             ftext = fmsg.msgid_plural or ""
0169         elif name == "msgstr":
0170             text = msg.msgstr[item]
0171             ftext = fmsg.msgstr[item]
0172         # TODO: Add more fields.
0173         else:
0174             warning(_("@info",
0175                       "Unknown field '%(field)s' "
0176                       "in highlighting specification.",
0177                       field=name))
0178             continue
0179 
0180         if len(hspec) > 3:
0181             # Override filtered text from filtered message
0182             # by filtered text from the highlight spec.
0183             ftext = hspec[3]
0184 
0185         spans = adapt_spans(text, ftext, spans, merge=False)
0186 
0187         if msg.msgid_plural is not None and name == "msgstr":
0188             name = "%s_%d" % (name, item)
0189 
0190         for span in spans:
0191             if len(span) < 3:
0192                 continue
0193             start, end, snote = span
0194             if isinstance(start, int) and isinstance(end, int):
0195                 seglen = end - start
0196                 if seglen > 0:
0197                     segtext = text[start:end]
0198                     if len(segtext) > 30:
0199                         segtext = segtext[:27] + "..."
0200                     posinfo = "%s:%d:\"%s\"" % (name, start, escape(segtext))
0201                 else:
0202                     posinfo = "%s:%d" % (name, start)
0203             else:
0204                 posinfo = "%s" % name
0205             posinfo = ColorString("<green>%s</green>") % posinfo
0206 
0207             rtext = cinterp("%s[%s]: %s", refpos, posinfo, snote)
0208             report(rtext, subsrc=subsrc, showcmd=False)
0209 
0210 
0211 def report_msg_to_lokalize (msg, cat, report=None):
0212     """
0213     Open catalog in Lokalize and jump to message.
0214 
0215     Lokalize is a CAT tool for KDE 4, U{http://userbase.kde.org/Lokalize}.
0216     This function opens the catalog in Lokalize (if not already open)
0217     and jumps to the given message within it.
0218 
0219     If the message is obsolete, it will be ignored.
0220 
0221     @param msg: the message which should be jumped to in Lokalize
0222     @type msg: L{Message_base}
0223     @param cat: the catalog in which the message resides
0224     @type cat: L{Catalog}
0225     @param report: simple text or highlight specification
0226     @type report: string or L{highlight<report_msg_content>}
0227     """
0228 
0229     dbus = _get_module("dbus",
0230                        _("@info",
0231                          "Communication with Lokalize not possible. "
0232                          "Try installing the '%(pkg)s' package.",
0233                          pkg="python-dbus"))
0234     if not dbus: return
0235 
0236     if msg.obsolete: return
0237 
0238     # If report is a highlight specification,
0239     # flatten it into lines of notes by spans.
0240     if isinstance(report, list):
0241         notes=[]
0242         for hspec in report:
0243             for span in hspec[2]:
0244                 if len(span) > 2:
0245                     notes.append(span[2])
0246         report = cjoin(notes, "\n")
0247 
0248     try:
0249         try: globals()['lokalizeobj']
0250         except:
0251             bus = dbus.SessionBus()
0252             lokalize_dbus_instances=lambda:[name for name in bus.list_names() if name.startswith('org.kde.lokalize')]
0253             for lokalize_dbus_instance in lokalize_dbus_instances():
0254                 try:
0255                     globals()['lokalizeinst']=lokalize_dbus_instance
0256                     globals()['lokalizeobj']=bus.get_object(globals()['lokalizeinst'],'/ThisIsWhatYouWant')
0257                     globals()['openFileInEditor']=globals()['lokalizeobj'].get_dbus_method('openFileInEditor','org.kde.Lokalize.MainWindow')
0258                     globals()['visitedcats']={}
0259                 except:
0260                     pass
0261             if 'openFileInEditor' not in globals():
0262                 return
0263 
0264         index=globals()['openFileInEditor'](os.path.abspath(cat.filename))
0265         editorobj=dbus.SessionBus().get_object(globals()['lokalizeinst'],'/ThisIsWhatYouWant/Editor/%d' % index)
0266 
0267         if cat.filename not in globals()['visitedcats']:
0268             globals()['visitedcats'][cat.filename]=1
0269 
0270             setEntriesFilteredOut=editorobj.get_dbus_method('setEntriesFilteredOut','org.kde.Lokalize.Editor')
0271             setEntriesFilteredOut(True)
0272 
0273         setEntryFilteredOut=editorobj.get_dbus_method('setEntryFilteredOut','org.kde.Lokalize.Editor')
0274         setEntryFilteredOut(msg.refentry-1,False)
0275 
0276         gotoEntry=editorobj.get_dbus_method('gotoEntry','org.kde.Lokalize.Editor')
0277         gotoEntry(msg.refentry-1)
0278 
0279         if report:
0280             addTemporaryEntryNote=editorobj.get_dbus_method('addTemporaryEntryNote','org.kde.Lokalize.Editor')
0281             addTemporaryEntryNote(msg.refentry-1,report.resolve(ctype="none"))
0282 
0283     except:
0284         return
0285 
0286 
0287 def report_msg_content (msg, cat,
0288                         wrapf=None, force=False,
0289                         note=None, delim=None, highlight=None,
0290                         showmsg=True, fmsg=None, showfmsg=False,
0291                         subsrc=None, file=sys.stdout):
0292     """
0293     Report the content of a PO message.
0294 
0295     Provides the message reference, consisting of the catalog name and
0296     the message position within it, the message contents,
0297     and any notes on particular segments.
0298 
0299     Parts of the message can be highlighted using colors.
0300     Parameter C{highlight} provides the highlighting specification, as
0301     list of tuples where each tuple consists of: name of the message element
0302     to highlight, element index (used when the element is a list of values),
0303     list of spans, and optionally the filtered text of the element value.
0304     For example, to highlight spans C{(5, 10)} and C{(15, 25)} in the C{msgid},
0305     and C{(30, 40)} in C{msgstr}, the highlighting specification would be::
0306 
0307         [("msgid", 0, [(5, 10), (15, 25)]), ("msgstr", 0, [(30, 40)])]
0308 
0309     Names of the elements that can presently be highlighted are: C{"msgctxt"},
0310     C{"msgid"}, C{"msgid_plural"}, C{"msgstr"}, C{"manual_comment"},
0311     C{"auto_comment"}, C{"source"}, C{"flag"}.
0312     For unique fields the element index is not used, but 0 should be given
0313     for consistency (may be enforced later).
0314     Span tuples can have a third element, following the indices, which is
0315     the note about why the particular span is highlighted;
0316     there may be more elements after the note, and these are all ignored.
0317     If start or end index of a span is not an integer,
0318     then the note is taken as relating to the complete field.
0319 
0320     Sometimes the match to which the spans correspond has been made on a
0321     filtered value of the message field (e.g. after accelerator markers
0322     or tags have been removed). In that case, the filtered text can be
0323     given as the fourth element of the tuple, after the list of spans, and
0324     the function will try to fit spans from filtered onto original text.
0325     More globally, if the complete highlight is relative to a modified,
0326     filtered version of the message, this message can be given as
0327     C{fmsg} parameter.
0328 
0329     The display of content can be controlled by C{showmsg} parameter;
0330     if it is C{False}, only the message reference and span notes are shown.
0331     Similarly for the C{showfmsg} parameter, which controls the display
0332     of the content of filtered message (if given by C{fmsg}).
0333     To show the filtered message may be useful for debugging filtering
0334     in cases when it is not straightforward, or it is user-defined.
0335 
0336     @param msg: the message to report the content for
0337     @type msg: L{Message_base}
0338     @param cat: the catalog where the message lives
0339     @type cat: L{Catalog} or C{None}
0340     @param wrapf:
0341         the function used for wrapping message fields in output.
0342         See L{to_lines()<message.Message_base.to_lines>} method
0343         of message classes for details.
0344         If not given, it will be taken from the catalog
0345         (see L{Catalog.wrapf<catalog.Catalog.wrapf>}).
0346     @type wrapf: (string)->[string...]
0347     @param force: whether to force reformatting of cached message content
0348     @type force: bool
0349     @param note: note about why the content is being reported
0350     @type note: string
0351     @param delim: text to print on the line following the message
0352     @type delim: C{None} or string
0353     @param highlight: highlighting specification of message elements
0354     @type highlight: (see description)
0355     @param showmsg: show content of the message
0356     @type showmsg: bool
0357     @param fmsg: filtered message
0358     @type fmsg: L{Message_base}
0359     @param showfmsg: show content of the filtered message, if any
0360     @type showfmsg: bool
0361     @param subsrc: more detailed source of the message
0362     @type subsrc: C{None} or string
0363     @param file: output stream
0364     @type file: file
0365     """
0366 
0367     rsegs = []
0368 
0369     wrapf = wrapf or cat.wrapf()
0370 
0371     notes_data = []
0372     if highlight:
0373         msg = Message(msg) # must work on copy, highlight modifies it
0374         ffmsg = fmsg or msg # use original message as filtered if not given
0375 
0376         # Unify spans for same parts, to have single coloring pass per part
0377         # (otherwise markup can get corrupted).
0378         highlightd = {}
0379         for hspec in highlight:
0380             name, item, spans = hspec[:3]
0381             pkey = (name, item)
0382             phspec = highlightd.get(pkey)
0383             if phspec is None:
0384                 # Make needed copies in order not to modify
0385                 # the original highlight when adding stuff later.
0386                 highlightd[pkey] = list(hspec)
0387                 highlightd[pkey][2] = list(spans)
0388             else:
0389                 phspec[2].extend(spans)
0390                 # Take filtered text if available and not already taken.
0391                 if len(hspec) > 3 and len(phspec) <= 3:
0392                     phspec.append(hspec[3])
0393         highlight = list(highlightd.values())
0394 
0395         for hspec in highlight:
0396             name, item, spans = hspec[:3]
0397 
0398             def hl (text, ftext):
0399                 if len(hspec) > 3:
0400                     # Override filtered text from filtered message
0401                     # by filtered text from the highlight spec.
0402                     ftext = hspec[3]
0403                 aspans = adapt_spans(text, ftext, spans, merge=False)
0404                 notes_data.append((text, name, item, aspans))
0405                 text = _highlight_spans(text, spans, "red", ftext=ftext)
0406                 return text
0407 
0408             if name == "msgctxt":
0409                 if msg.msgctxt or ffmsg.msgctxt:
0410                     msg.msgctxt = hl(msg.msgctxt or "", ffmsg.msgctxt or "")
0411             elif name == "msgid":
0412                 msg.msgid = hl(msg.msgid, ffmsg.msgid)
0413             elif name == "msgid_plural":
0414                 msg.msgid_plural = hl(msg.msgid_plural or "",
0415                                       ffmsg.msgid_plural or "")
0416             elif name == "msgstr":
0417                 msg.msgstr[item] = hl(msg.msgstr[item], ffmsg.msgstr[item])
0418             elif name == "manual_comment":
0419                 msg.manual_comment[item] = hl(msg.manual_comment[item],
0420                                               ffmsg.manual_comment[item])
0421             elif name == "auto_comment":
0422                 msg.auto_comment[item] = hl(msg.auto_comment[item],
0423                                             ffmsg.auto_comment[item])
0424             elif name == "source":
0425                 msg.source[item] = Monpair((hl(msg.source[item][0],
0426                                               ffmsg.source[item][0]),
0427                                             msg.source[item][1]))
0428             elif name == "flag":
0429                 pass # FIXME: How to do this?
0430             else:
0431                 warning(_("@info",
0432                           "Unknown field '%(field)s' "
0433                           "in highlighting specification.",
0434                           field=name))
0435 
0436     # Report the message.
0437     msegs = []
0438     if cat is not None:
0439         msegs += [_msg_pos_fmt(cat.filename, msg.refline, msg.refentry) + "\n"]
0440     if showmsg:
0441         msgstr = msg.to_string(wrapf=wrapf, force=force, colorize=1)
0442         msegs += [msgstr.rstrip() + "\n"]
0443     if msegs:
0444         rsegs.append(cjoin(msegs).rstrip())
0445 
0446     # Report notes.
0447     if note is not None: # global
0448         notestr = _("@info",
0449                     "<bold>[note]</bold> %(msg)s",
0450                     msg=note)
0451         rsegs.append(notestr)
0452     if notes_data: # span notes
0453         note_ord = 1
0454         for text, name, item, spans in notes_data:
0455             if msg.msgid_plural is not None and name == "msgstr":
0456                 name = "%s_%d" % (name, item)
0457             for span in spans:
0458                 if len(span) < 3:
0459                     continue
0460                 start, end, snote = span
0461                 if isinstance(start, int) and isinstance(end, int):
0462                     seglen = end - start
0463                     if seglen > 0:
0464                         segtext = text[start:end]
0465                         if len(segtext) > 30:
0466                             segtext = _("@item:intext shortened longer text",
0467                                         "%(snippet)s...",
0468                                         snippet=segtext[:27])
0469                         posinfo = "%s:%d:\"%s\"" % (name, start, escape(segtext))
0470                     else:
0471                         posinfo = "%s:%d" % (name, start)
0472                 else:
0473                     posinfo = "%s" % name
0474                 posinfo = ColorString("<green>%s</green>") % posinfo
0475                 rsegs.append(_("@info",
0476                                "[%(pos)s]: %(msg)s",
0477                                pos=posinfo, msg=snote))
0478                 note_ord += 1
0479 
0480     # Report the filtered message, if given and requested.
0481     if fmsg and showfmsg:
0482         fmtnote = (ColorString("<green>%s</green>")
0483                    % _("@info", ">>> Filtered message was:"))
0484         rsegs.append(fmtnote)
0485         fmsgstr = fmsg.to_string(wrapf=wrapf, force=force, colorize=1)
0486         mstr = fmsgstr.rstrip() + "\n"
0487         rsegs.append(mstr.rstrip())
0488 
0489     if delim:
0490         rsegs.append(delim)
0491 
0492     rtext = cjoin(rsegs, "\n").rstrip()
0493     report(rtext, subsrc=subsrc, file=file)
0494 
0495 
0496 def rule_error(msg, cat, rule, highlight=None, fmsg=None, showmsg=True,
0497                predelim=False):
0498     """
0499     Print formated rule error message on screen.
0500 
0501     @param msg: pology.message.Message object
0502     @param cat: pology.catalog.Catalog object
0503     @param rule: pology.rules.Rule object
0504     @param highlight: highlight specification (see L{report_msg_content})
0505     @param fmsg: filtered message which the rule really matched
0506     @param showmsg: whether to show contents of message (either filtered or original)
0507     @param predelim: whether to also print delimiter before the rule error
0508     """
0509 
0510     # Some info on the rule.
0511     rinfo = _("@info",
0512               "rule %(rule)s <bold><red>==></red></bold> "
0513               "<bold>%(msg)s</bold>",
0514               rule=rule.displayName, msg=rule.hint)
0515 
0516     if showmsg:
0517         delim = "-" * 40
0518         if predelim:
0519             report(delim)
0520         report_msg_content(msg, cat,
0521                            highlight=highlight,
0522                            fmsg=fmsg, showfmsg=(fmsg is not None),
0523                            note=rinfo, delim=delim)
0524     else:
0525         report_on_msg(rinfo, msg, cat)
0526         report_on_msg_hl(highlight, msg, cat, fmsg)
0527 
0528 
0529 def multi_rule_error (msg, cat, rspec, showmsg=True, predelim=False):
0530     """
0531     Print formated rule error messages on screen.
0532 
0533     Like L{rule_error}, but reports multiple failed rules at once.
0534     Contents of the matched message is shown only once for all rules,
0535     with all highlights embedded, and all rule information following.
0536     This holds unless there are several different filtered messages,
0537     when rule failures are reported in groups by filtered message.
0538 
0539     @param msg: the message matched by rules
0540     @type msg: Message
0541     @param cat: the catalog in which the message resides
0542     @type cat: Catalog
0543     @param rspec: specification of failed rules. This is a list in which
0544         each element can be one of:
0545           - rule
0546           - tuple of rule and highlight specification (see
0547             L{report_msg_content} for details on highlight specifications).
0548             Highlight can be None.
0549           - tuple of rule, highlight, and filtered message which
0550             the rule really matched.
0551             Highlight and filtered message can be None.
0552     @type rspec: [(Rule|(Rule, highlight)|(Rule, highlight, Message))*]
0553     @param showmsg: whether to show contents of message
0554         (both original and filtered if given)
0555     @type showmsg: bool
0556     @param predelim: whether to also print delimiter before the first error
0557     @type predelim: bool
0558     """
0559 
0560     # Expand elements in rule specification to full lengths.
0561     rspec_mod = []
0562     for el in rspec:
0563         if not isinstance(el, tuple):
0564             el = (el,)
0565         el_mod = el + tuple(None for i in range(3 - len(el)))
0566         rspec_mod.append(el_mod)
0567     rspec = rspec_mod
0568 
0569     # Split into groups by distinct filtered messages,
0570     # or make one dummy group if content display not requested.
0571     if showmsg:
0572         rspec_groups = []
0573         for rule, hl, fmsg in rspec:
0574             rlhls = None
0575             for ofmsg, rlhls in rspec_groups:
0576                 if fmsg == ofmsg: # check for apparent equality
0577                     break
0578             if rlhls is None:
0579                 rlhls = []
0580                 rspec_groups.append((fmsg, rlhls))
0581             rlhls.append((rule, hl))
0582     else:
0583         rlhls = []
0584         rspec_groups = [(None, rlhls)]
0585         for rule, hl, fmsg in rspec:
0586             rlhls.append((rule, hl))
0587 
0588     # Report each rule group.
0589     for fmsg, rlhls in rspec_groups:
0590         rinfos = []
0591         highlight = []
0592         for rule, hl in rlhls:
0593             rinfos.append(_("@info",
0594                             "rule %(rule)s <bold><red>==></red></bold> "
0595                             "<bold>%(msg)s</bold>",
0596                             rule=rule.displayName, msg=rule.hint))
0597             highlight.extend(hl)
0598         if len(rinfos) > 1:
0599             note = cjoin([""] + rinfos, "\n")
0600         elif rinfos:
0601             note = rinfos[0]
0602         if showmsg:
0603             delim = "-" * 40
0604             if predelim:
0605                 report(delim)
0606             report_msg_content(msg, cat,
0607                                highlight=highlight,
0608                                fmsg=fmsg, showfmsg=(fmsg is not None),
0609                                note=note, delim=delim)
0610         else:
0611             report_on_msg(note, msg, cat)
0612             report_on_msg_hl(highlight, msg, cat, fmsg)
0613 
0614 
0615 def rule_xml_error(msg, cat, rule, span, pluralId=0):
0616     """Create and returns rule error message in XML format
0617     @param msg: pology.message.Message object
0618     @param cat: pology.catalog.Catalog object
0619     @param span: list of 2-tuple (start, end) of offending spans
0620     @param rule: pology.rules.Rule object
0621     @param pluralId: msgstr count in case of plural form. Default to 0
0622     @return: XML message as a list of unicode string"""
0623     xmlError=[]
0624     xmlError.append("\t<error>\n")
0625     xmlError.append("\t\t<line>%s</line>\n" % msg.refline)
0626     xmlError.append("\t\t<refentry>%s</refentry>\n" % msg.refentry)
0627     xmlError.append("\t\t<msgctxt><![CDATA[%s]]></msgctxt>\n" % _escapeCDATA(msg.msgctxt or ""))
0628     xmlError.append("\t\t<msgid><![CDATA[%s]]></msgid>\n" % _escapeCDATA(msg.msgid))
0629     xmlError.append("\t\t<msgstr><![CDATA[%s]]></msgstr>\n" % _escapeCDATA(msg.msgstr[pluralId]))
0630     for begin, end in span:
0631         if isinstance(begin, int) and isinstance(end, int):
0632             xmlError.append("\t\t<highlight begin='%s' end='%s'/>\n" % (begin, end))
0633     #xmlError.append("\t\t<start>%s</start>\n" % span[0])
0634     #xmlError.append("\t\t<end>%s</end>\n" % span[1])
0635     xmlError.append("\t\t<pattern><![CDATA[%s]]></pattern>\n" % rule.rawPattern)
0636     xmlError.append("\t\t<hint><![CDATA[%s]]></hint>\n" % rule.hint)
0637     xmlError.append("\t</error>\n")
0638     return xmlError
0639 
0640 
0641 def spell_error(msg, cat, faultyWord, suggestions):
0642     """Print formated rule error message on screen
0643     @param msg: pology.message.Message object
0644     @param cat: pology.catalog.Catalog object
0645     @param faultyWord: badly spelled word
0646     @param suggestions : list of correct words to suggest"""
0647     report("-"*40)
0648     report(ColorString("<bold>%s:%d(%d)</bold>")
0649            % (cat.filename, msg.refline, msg.refentry))
0650     if msg.msgctxt:
0651         report(_("@info",
0652                  "<bold>Context:</bold> %(snippet)s",
0653                  snippet=msg.msgctxt))
0654     #TODO: color in red part of context that make the mistake
0655     report(_("@info",
0656              "<bold>Faulty word:</bold> <red>%(word)s</red>",
0657              word=faultyWord))
0658     if suggestions:
0659         report(_("@info",
0660                  "<bold>Suggestions:</bold> %(wordlist)s",
0661                  wordlist=format_item_list(suggestions)))
0662 
0663 
0664 def spell_xml_error(msg, cat, faultyWord, suggestions, pluralId=0):
0665     """Create and returns spell error message in XML format
0666     @param msg: pology.message.Message object
0667     @param cat: pology.catalog.Catalog object
0668     @param faultyWord: badly spelled word
0669     @param suggestions : list of correct words to suggest
0670     @param pluralId: msgstr count in case of plural form. Default to 0
0671     @return: XML message as a list of unicode string"""
0672     xmlError=[]
0673     xmlError.append("\t<error>\n")
0674     xmlError.append("\t\t<line>%s</line>\n" % msg.refline)
0675     xmlError.append("\t\t<refentry>%s</refentry>\n" % msg.refentry)
0676     xmlError.append("\t\t<msgctxt><![CDATA[%s]]></msgctxt>\n" % _escapeCDATA(msg.msgctxt or ""))
0677     xmlError.append("\t\t<msgid><![CDATA[%s]]></msgid>\n" % _escapeCDATA(msg.msgid))
0678     xmlError.append("\t\t<msgstr><![CDATA[%s]]></msgstr>\n" % _escapeCDATA(msg.msgstr[pluralId]))
0679     xmlError.append("\t\t<faulty>%s</faulty>\n" % faultyWord)
0680     for suggestion in suggestions:
0681         xmlError.append("\t\t<suggestion>%s</suggestion>\n" % suggestion)
0682     xmlError.append("\t</error>\n")
0683     return xmlError
0684 
0685 
0686 # Format string for message reference, based on the file descriptor.
0687 def _msg_pos_fmt (path, line, col):
0688 
0689     return (ColorString("<cyan>%s</cyan>:<purple>%d</purple>"
0690                         "(<purple>#%d</purple>)") % (path, line, col))
0691 
0692 
0693 def _escapeCDATA(text):
0694     """Escape CDATA tags to allow inclusion into CDATA
0695     @param text: text to convert
0696     @type text: str or unicode
0697     @return: modified string"""
0698     text=text.replace("<![CDATA[", "<_!_[CDATA[")
0699     text=text.replace("]]>", "]_]_>")
0700     return text
0701 
0702 
0703 def _highlight_spans (text, spans, color, ftext=None):
0704     """
0705     Adds colors around highlighted spans in text.
0706 
0707     Spans are given as list of index tuples C{[(start1, end1), ...]} where
0708     start and end index have standard Python semantics.
0709     Span tuples can have more than two elements, with indices followed by
0710     additional elements, which are ignored by this function.
0711     If start or end index in a span is not an integer, the span is ignored.
0712 
0713     The C{color} parameter is one of the color tags available in
0714     L{ColorString<colors.ColorString>} markup.
0715 
0716     If C{ftext} is not C{None}, spans are understood as relative to it,
0717     and the function will try to adapt them to the main text
0718     (see L{pology.diff.adapt_spans}).
0719 
0720     @param text: text to be highlighted
0721     @type text: string
0722     @param spans: spans to highlight
0723     @type spans: list of tuples
0724     @param color: color tag
0725     @type color: string
0726     @param ftext: text to which spans are actually relative
0727     @type ftext: string
0728 
0729     @returns: highlighted text
0730     @rtype: string
0731     """
0732 
0733     if not spans or color is None:
0734         return text
0735 
0736     # Adapt spans regardless if filtered text has been given or not,
0737     # to fix any overlapping and put into expected ordering.
0738     if ftext is None:
0739         ftext = text
0740     spans = adapt_spans(text, ftext, spans, merge=True)
0741     if not spans:
0742         return text
0743 
0744     ctext = ""
0745     cstart = 0
0746     for span in spans:
0747         if not isinstance(span[0], int) or not isinstance(span[1], int):
0748             continue
0749         ctext += text[cstart:span[0]]
0750         ctext += (ColorString("<%s>%%s</%s>" % (color, color))
0751                   % text[span[0]:span[1]]) # outside, to have auto-escaping
0752         cstart = span[1]
0753     ctext += text[span[1]:]
0754 
0755     return ctext
0756