details: http://www.bx.psu.edu/hg/galaxy/rev/9c325f3656b5 changeset: 2617:9c325f3656b5 user: James Taylor <james@jamestaylor.org> date: Mon Aug 24 15:53:17 2009 -0400 description: Support for user defined pages. Page content is stripped down XHTML. Pages can be edited using a WYM editor, and have stable public urls. This is VERY PRELIMINARY, but functional 67 file(s) affected in this change: lib/galaxy/model/__init__.py lib/galaxy/model/mapping.py lib/galaxy/model/migrate/versions/0014_pages.py lib/galaxy/util/sanitize_html.py lib/galaxy/web/__init__.py lib/galaxy/web/buildapp.py lib/galaxy/web/controllers/page.py lib/galaxy/web/controllers/user.py lib/galaxy/web/framework/helpers/grids.py static/scripts/jquery.wymeditor.js static/wymeditor/iframe/default/lbl-blockquote.png static/wymeditor/iframe/default/lbl-h1.png static/wymeditor/iframe/default/lbl-h2.png static/wymeditor/iframe/default/lbl-h3.png static/wymeditor/iframe/default/lbl-h4.png static/wymeditor/iframe/default/lbl-h5.png static/wymeditor/iframe/default/lbl-h6.png static/wymeditor/iframe/default/lbl-p.png static/wymeditor/iframe/default/lbl-pre.png static/wymeditor/iframe/default/wymiframe.css static/wymeditor/iframe/default/wymiframe.html static/wymeditor/iframe/galaxy/lbl-blockquote.png static/wymeditor/iframe/galaxy/lbl-h1.png static/wymeditor/iframe/galaxy/lbl-h2.png static/wymeditor/iframe/galaxy/lbl-h3.png static/wymeditor/iframe/galaxy/lbl-h4.png static/wymeditor/iframe/galaxy/lbl-h5.png static/wymeditor/iframe/galaxy/lbl-h6.png static/wymeditor/iframe/galaxy/lbl-p.png static/wymeditor/iframe/galaxy/lbl-pre.png static/wymeditor/iframe/galaxy/wymiframe.css static/wymeditor/iframe/galaxy/wymiframe.html static/wymeditor/lang/bg.js static/wymeditor/lang/ca.js static/wymeditor/lang/cs.js static/wymeditor/lang/de.js static/wymeditor/lang/en.js static/wymeditor/lang/es.js static/wymeditor/lang/fa.js static/wymeditor/lang/fi.js static/wymeditor/lang/fr.js static/wymeditor/lang/he.js static/wymeditor/lang/hr.js static/wymeditor/lang/hu.js static/wymeditor/lang/it.js static/wymeditor/lang/nb.js static/wymeditor/lang/nl.js static/wymeditor/lang/nn.js static/wymeditor/lang/pl.js static/wymeditor/lang/pt-br.js static/wymeditor/lang/pt.js static/wymeditor/lang/ru.js static/wymeditor/lang/sv.js static/wymeditor/lang/tr.js static/wymeditor/lang/zh_cn.js static/wymeditor/skins/default/icons.png static/wymeditor/skins/default/skin.css static/wymeditor/skins/default/skin.js static/wymeditor/skins/galaxy/icons.png static/wymeditor/skins/galaxy/skin.css static/wymeditor/skins/galaxy/skin.js templates/grid.mako templates/page/create.mako templates/page/display.mako templates/page/editor.mako templates/page/index.mako templates/user/index.mako diffs (truncated from 7896 to 3000 lines): diff -r 4ff4be2f436f -r 9c325f3656b5 lib/galaxy/model/__init__.py --- a/lib/galaxy/model/__init__.py Mon Aug 24 15:27:27 2009 -0400 +++ b/lib/galaxy/model/__init__.py Mon Aug 24 15:53:17 2009 -0400 @@ -33,6 +33,7 @@ self.external = False self.deleted = False self.purged = False + self.username = None # Relationships self.histories = [] @@ -1118,7 +1119,20 @@ self.country+'<br/>'+ \ 'Phone: '+self.phone +class Page( object ): + def __init__( self ): + self.id = None + self.user = None + self.title = None + self.slug = None + self.latest_revision_id = None + self.revisions = [] +class PageRevision( object ): + def __init__( self ): + self.user = None + self.title = None + self.content = None diff -r 4ff4be2f436f -r 9c325f3656b5 lib/galaxy/model/mapping.py --- a/lib/galaxy/model/mapping.py Mon Aug 24 15:27:27 2009 -0400 +++ b/lib/galaxy/model/mapping.py Mon Aug 24 15:53:17 2009 -0400 @@ -42,6 +42,7 @@ Column( "create_time", DateTime, default=now ), Column( "update_time", DateTime, default=now, onupdate=now ), Column( "email", TrimmedString( 255 ), nullable=False ), + Column( "username", TrimmedString( 255 ), index=True, unique=True ), Column( "password", TrimmedString( 40 ), nullable=False ), Column( "external", Boolean, default=False ), Column( "deleted", Boolean, index=True, default=False ), @@ -523,6 +524,26 @@ Column( "sample_state_id", Integer, ForeignKey( "sample_state.id" ), index=True ), Column( "comment", TEXT ) ) +Page.table = Table( "page", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True, nullable=False ), + Column( "latest_revision_id", Integer, + ForeignKey( "page_revision.id", use_alter=True, name='page_latest_revision_id_fk' ), index=True ), + Column( "title", TEXT ), + Column( "slug", TEXT, unique=True, index=True ), + ) + +PageRevision.table = Table( "page_revision", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "page_id", Integer, ForeignKey( "page.id" ), index=True, nullable=False ), + Column( "title", TEXT ), + Column( "content", TEXT ) + ) + # With the tables defined we can define the mappers and setup the # relationships between the model objects. @@ -905,6 +926,18 @@ assign_mapper( context, MetadataFile, MetadataFile.table, properties=dict( history_dataset=relation( HistoryDatasetAssociation ), library_dataset=relation( LibraryDatasetDatasetAssociation ) ) ) +assign_mapper( context, PageRevision, PageRevision.table ) + +assign_mapper( context, Page, Page.table, + properties=dict( user=relation( User ), + revisions=relation( PageRevision, backref='page', + cascade="all, delete-orphan", + primaryjoin=( Page.table.c.id == PageRevision.table.c.page_id ) ), + latest_revision=relation( PageRevision, post_update=True, + primaryjoin=( Page.table.c.latest_revision_id == PageRevision.table.c.id ), + lazy=False ) + ) ) + def db_next_hid( self ): """ Override __next_hid to generate from the database in a concurrency diff -r 4ff4be2f436f -r 9c325f3656b5 lib/galaxy/model/migrate/versions/0014_pages.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/model/migrate/versions/0014_pages.py Mon Aug 24 15:53:17 2009 -0400 @@ -0,0 +1,55 @@ +from sqlalchemy import * +from migrate import * +from migrate.changeset import * + +import datetime +now = datetime.datetime.utcnow + +import logging +log = logging.getLogger( __name__ ) + +metadata = MetaData( migrate_engine ) + +Page_table = Table( "page", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True, nullable=False ), + Column( "latest_revision_id", Integer, + ForeignKey( "page_revision.id", use_alter=True, name='page_latest_revision_id_fk' ), index=True ), + Column( "title", TEXT ), + Column( "slug", TEXT, unique=True, index=True ), + ) + +PageRevision_table = Table( "page_revision", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "page_id", Integer, ForeignKey( "page.id" ), index=True, nullable=False ), + Column( "title", TEXT ), + Column( "content", TEXT ) + ) + +def upgrade(): + metadata.reflect() + try: + Page_table.create() + except: + log.debug( "Could not create page table" ) + try: + PageRevision_table.create() + except: + log.debug( "Could not create page_revision table" ) + + # Add 1 column to the user table + User_table = Table( "galaxy_user", metadata, autoload=True ) + col = Column( 'username', String(255), index=True, unique=True, default=False ) + col.create( User_table ) + assert col is User_table.c.username + +def downgrade(): + metadata.reflect() + Page_table.drop() + PageRevision_table.drop() + User_table = Table( "galaxy_user", metadata, autoload=True ) + User_table.c.username.drop() diff -r 4ff4be2f436f -r 9c325f3656b5 lib/galaxy/util/sanitize_html.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/util/sanitize_html.py Mon Aug 24 15:53:17 2009 -0400 @@ -0,0 +1,439 @@ +""" +HTML Sanitizer (ripped from feedparser) +""" + +import re, sgmllib + +# reversable htmlentitydefs mappings for Python 2.2 +try: + from htmlentitydefs import name2codepoint, codepoint2name +except: + import htmlentitydefs + name2codepoint={} + codepoint2name={} + for (name,codepoint) in htmlentitydefs.entitydefs.iteritems(): + if codepoint.startswith(''): codepoint=unichr(int(codepoint[2:-1])) + name2codepoint[name]=ord(codepoint) + codepoint2name[ord(codepoint)]=name + +_cp1252 = { + unichr(128): unichr(8364), # euro sign + unichr(130): unichr(8218), # single low-9 quotation mark + unichr(131): unichr( 402), # latin small letter f with hook + unichr(132): unichr(8222), # double low-9 quotation mark + unichr(133): unichr(8230), # horizontal ellipsis + unichr(134): unichr(8224), # dagger + unichr(135): unichr(8225), # double dagger + unichr(136): unichr( 710), # modifier letter circumflex accent + unichr(137): unichr(8240), # per mille sign + unichr(138): unichr( 352), # latin capital letter s with caron + unichr(139): unichr(8249), # single left-pointing angle quotation mark + unichr(140): unichr( 338), # latin capital ligature oe + unichr(142): unichr( 381), # latin capital letter z with caron + unichr(145): unichr(8216), # left single quotation mark + unichr(146): unichr(8217), # right single quotation mark + unichr(147): unichr(8220), # left double quotation mark + unichr(148): unichr(8221), # right double quotation mark + unichr(149): unichr(8226), # bullet + unichr(150): unichr(8211), # en dash + unichr(151): unichr(8212), # em dash + unichr(152): unichr( 732), # small tilde + unichr(153): unichr(8482), # trade mark sign + unichr(154): unichr( 353), # latin small letter s with caron + unichr(155): unichr(8250), # single right-pointing angle quotation mark + unichr(156): unichr( 339), # latin small ligature oe + unichr(158): unichr( 382), # latin small letter z with caron + unichr(159): unichr( 376)} # latin capital letter y with diaeresis + +class _BaseHTMLProcessor(sgmllib.SGMLParser): + special = re.compile('''[<>'"]''') + bare_ampersand = re.compile("&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)") + elements_no_end_tag = ['area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', + 'img', 'input', 'isindex', 'link', 'meta', 'param'] + + def __init__(self, encoding, type): + self.encoding = encoding + self.type = type + ## if _debug: sys.stderr.write('entering BaseHTMLProcessor, encoding=%s\n' % self.encoding) + sgmllib.SGMLParser.__init__(self) + + def reset(self): + self.pieces = [] + sgmllib.SGMLParser.reset(self) + + def _shorttag_replace(self, match): + tag = match.group(1) + if tag in self.elements_no_end_tag: + return '<' + tag + ' />' + else: + return '<' + tag + '></' + tag + '>' + + def parse_starttag(self,i): + j=sgmllib.SGMLParser.parse_starttag(self, i) + if self.type == 'application/xhtml+xml': + if j>2 and self.rawdata[j-2:j]=='/>': + self.unknown_endtag(self.lasttag) + return j + + def feed(self, data): + data = re.compile(r'<!((?!DOCTYPE|--|\[))', re.IGNORECASE).sub(r'<!\1', data) + #data = re.sub(r'<(\S+?)\s*?/>', self._shorttag_replace, data) # bug [ 1399464 ] Bad regexp for _shorttag_replace + data = re.sub(r'<([^<>\s]+?)\s*/>', self._shorttag_replace, data) + data = data.replace(''', "'") + data = data.replace('"', '"') + if self.encoding and type(data) == type(u''): + data = data.encode(self.encoding) + sgmllib.SGMLParser.feed(self, data) + sgmllib.SGMLParser.close(self) + + def normalize_attrs(self, attrs): + if not attrs: return attrs + # utility method to be called by descendants + attrs = dict([(k.lower(), v) for k, v in attrs]).items() + attrs = [(k, k in ('rel', 'type') and v.lower() or v) for k, v in attrs] + attrs.sort() + return attrs + + def unknown_starttag(self, tag, attrs): + # called for each start tag + # attrs is a list of (attr, value) tuples + # e.g. for <pre class='screen'>, tag='pre', attrs=[('class', 'screen')] + ## if _debug: sys.stderr.write('_BaseHTMLProcessor, unknown_starttag, tag=%s\n' % tag) + uattrs = [] + strattrs='' + if attrs: + for key, value in attrs: + value=value.replace('>','>').replace('<','<').replace('"','"') + value = self.bare_ampersand.sub("&", value) + # thanks to Kevin Marks for this breathtaking hack to deal with (valid) high-bit attribute values in UTF-8 feeds + if type(value) != type(u''): + try: + value = unicode(value, self.encoding) + except: + value = unicode(value, 'iso-8859-1') + uattrs.append((unicode(key, self.encoding), value)) + strattrs = u''.join([u' %s="%s"' % (key, value) for key, value in uattrs]) + if self.encoding: + try: + strattrs=strattrs.encode(self.encoding) + except: + pass + if tag in self.elements_no_end_tag: + self.pieces.append('<%(tag)s%(strattrs)s />' % locals()) + else: + self.pieces.append('<%(tag)s%(strattrs)s>' % locals()) + + def unknown_endtag(self, tag): + # called for each end tag, e.g. for </pre>, tag will be 'pre' + # Reconstruct the original end tag. + if tag not in self.elements_no_end_tag: + self.pieces.append("</%(tag)s>" % locals()) + + def handle_charref(self, ref): + # called for each character reference, e.g. for ' ', ref will be '160' + # Reconstruct the original character reference. + if ref.startswith('x'): + value = unichr(int(ref[1:],16)) + else: + value = unichr(int(ref)) + + if value in _cp1252.keys(): + self.pieces.append('%s;' % hex(ord(_cp1252[value]))[1:]) + else: + self.pieces.append('%(ref)s;' % locals()) + + def handle_entityref(self, ref): + # called for each entity reference, e.g. for '©', ref will be 'copy' + # Reconstruct the original entity reference. + if name2codepoint.has_key(ref): + self.pieces.append('&%(ref)s;' % locals()) + else: + self.pieces.append('&%(ref)s' % locals()) + + def handle_data(self, text): + # called for each block of plain text, i.e. outside of any tag and + # not containing any character or entity references + # Store the original text verbatim. + ## if _debug: sys.stderr.write('_BaseHTMLProcessor, handle_text, text=%s\n' % text) + self.pieces.append(text) + + def handle_comment(self, text): + # called for each HTML comment, e.g. <!-- insert Javascript code here --> + # Reconstruct the original comment. + self.pieces.append('<!--%(text)s-->' % locals()) + + def handle_pi(self, text): + # called for each processing instruction, e.g. <?instruction> + # Reconstruct original processing instruction. + self.pieces.append('<?%(text)s>' % locals()) + + def handle_decl(self, text): + # called for the DOCTYPE, if present, e.g. + # <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" + # "http://www.w3.org/TR/html4/loose.dtd"> + # Reconstruct original DOCTYPE + self.pieces.append('<!%(text)s>' % locals()) + + _new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*').match + def _scan_name(self, i, declstartpos): + rawdata = self.rawdata + n = len(rawdata) + if i == n: + return None, -1 + m = self._new_declname_match(rawdata, i) + if m: + s = m.group() + name = s.strip() + if (i + len(s)) == n: + return None, -1 # end of buffer + return name.lower(), m.end() + else: + self.handle_data(rawdata) +# self.updatepos(declstartpos, i) + return None, -1 + + def convert_charref(self, name): + return '%s;' % name + + def convert_entityref(self, name): + return '&%s;' % name + + def output(self): + '''Return processed HTML as a single string''' + return ''.join([str(p) for p in self.pieces]) + +class _HTMLSanitizer(_BaseHTMLProcessor): + acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', 'article', + 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', 'canvas', + 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'command', + 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', + 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', 'figure', 'footer', + 'font', 'form', 'header', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', + 'img', 'input', 'ins', 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', + 'menu', 'meter', 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', + 'option', 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', + 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', 'sub', + 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', 'th', 'thead', + 'tr', 'tt', 'u', 'ul', 'var', 'video', 'noscript'] + + acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey', + 'action', 'align', 'alt', 'autocomplete', 'autofocus', 'axis', + 'background', 'balance', 'bgcolor', 'bgproperties', 'border', + 'bordercolor', 'bordercolordark', 'bordercolorlight', 'bottompadding', + 'cellpadding', 'cellspacing', 'ch', 'challenge', 'char', 'charoff', + 'choff', 'charset', 'checked', 'cite', 'class', 'clear', 'color', 'cols', + 'colspan', 'compact', 'contenteditable', 'controls', 'coords', 'data', + 'datafld', 'datapagesize', 'datasrc', 'datetime', 'default', 'delay', + 'dir', 'disabled', 'draggable', 'dynsrc', 'enctype', 'end', 'face', 'for', + 'form', 'frame', 'galleryimg', 'gutter', 'headers', 'height', 'hidefocus', + 'hidden', 'high', 'href', 'hreflang', 'hspace', 'icon', 'id', 'inputmode', + 'ismap', 'keytype', 'label', 'leftspacing', 'lang', 'list', 'longdesc', + 'loop', 'loopcount', 'loopend', 'loopstart', 'low', 'lowsrc', 'max', + 'maxlength', 'media', 'method', 'min', 'multiple', 'name', 'nohref', + 'noshade', 'nowrap', 'open', 'optimum', 'pattern', 'ping', 'point-size', + 'prompt', 'pqg', 'radiogroup', 'readonly', 'rel', 'repeat-max', + 'repeat-min', 'replace', 'required', 'rev', 'rightspacing', 'rows', + 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size', 'span', 'src', + 'start', 'step', 'summary', 'suppress', 'tabindex', 'target', 'template', + 'title', 'toppadding', 'type', 'unselectable', 'usemap', 'urn', 'valign', + 'value', 'variable', 'volume', 'vspace', 'vrml', 'width', 'wrap', + 'xml:lang'] + + unacceptable_elements_with_end_tag = ['script', 'applet', 'style'] + + acceptable_css_properties = ['azimuth', 'background-color', + 'border-bottom-color', 'border-collapse', 'border-color', + 'border-left-color', 'border-right-color', 'border-top-color', 'clear', + 'color', 'cursor', 'direction', 'display', 'elevation', 'float', 'font', + 'font-family', 'font-size', 'font-style', 'font-variant', 'font-weight', + 'height', 'letter-spacing', 'line-height', 'overflow', 'pause', + 'pause-after', 'pause-before', 'pitch', 'pitch-range', 'richness', + 'speak', 'speak-header', 'speak-numeral', 'speak-punctuation', + 'speech-rate', 'stress', 'text-align', 'text-decoration', 'text-indent', + 'unicode-bidi', 'vertical-align', 'voice-family', 'volume', + 'white-space', 'width'] + + # survey of common keywords found in feeds + acceptable_css_keywords = ['auto', 'aqua', 'black', 'block', 'blue', + 'bold', 'both', 'bottom', 'brown', 'center', 'collapse', 'dashed', + 'dotted', 'fuchsia', 'gray', 'green', '!important', 'italic', 'left', + 'lime', 'maroon', 'medium', 'none', 'navy', 'normal', 'nowrap', 'olive', + 'pointer', 'purple', 'red', 'right', 'solid', 'silver', 'teal', 'top', + 'transparent', 'underline', 'white', 'yellow'] + + valid_css_values = re.compile('^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|' + + '\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$') + + mathml_elements = ['annotation', 'annotation-xml', 'maction', 'math', + 'merror', 'mfenced', 'mfrac', 'mi', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', + 'mphantom', 'mprescripts', 'mroot', 'mrow', 'mspace', 'msqrt', 'mstyle', + 'msub', 'msubsup', 'msup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', + 'munderover', 'none', 'semantics'] + + mathml_attributes = ['actiontype', 'align', 'columnalign', 'columnalign', + 'columnalign', 'close', 'columnlines', 'columnspacing', 'columnspan', 'depth', + 'display', 'displaystyle', 'encoding', 'equalcolumns', 'equalrows', + 'fence', 'fontstyle', 'fontweight', 'frame', 'height', 'linethickness', + 'lspace', 'mathbackground', 'mathcolor', 'mathvariant', 'mathvariant', + 'maxsize', 'minsize', 'open', 'other', 'rowalign', 'rowalign', 'rowalign', + 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'scriptlevel', 'selection', + 'separator', 'separators', 'stretchy', 'width', 'width', 'xlink:href', + 'xlink:show', 'xlink:type', 'xmlns', 'xmlns:xlink'] + + # svgtiny - foreignObject + linearGradient + radialGradient + stop + svg_elements = ['a', 'animate', 'animateColor', 'animateMotion', + 'animateTransform', 'circle', 'defs', 'desc', 'ellipse', 'foreignObject', + 'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern', + 'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', 'mpath', + 'path', 'polygon', 'polyline', 'radialGradient', 'rect', 'set', 'stop', + 'svg', 'switch', 'text', 'title', 'tspan', 'use'] + + # svgtiny + class + opacity + offset + xmlns + xmlns:xlink + svg_attributes = ['accent-height', 'accumulate', 'additive', 'alphabetic', + 'arabic-form', 'ascent', 'attributeName', 'attributeType', + 'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height', + 'class', 'color', 'color-rendering', 'content', 'cx', 'cy', 'd', 'dx', + 'dy', 'descent', 'display', 'dur', 'end', 'fill', 'fill-opacity', + 'fill-rule', 'font-family', 'font-size', 'font-stretch', 'font-style', + 'font-variant', 'font-weight', 'from', 'fx', 'fy', 'g1', 'g2', + 'glyph-name', 'gradientUnits', 'hanging', 'height', 'horiz-adv-x', + 'horiz-origin-x', 'id', 'ideographic', 'k', 'keyPoints', 'keySplines', + 'keyTimes', 'lang', 'mathematical', 'marker-end', 'marker-mid', + 'marker-start', 'markerHeight', 'markerUnits', 'markerWidth', 'max', + 'min', 'name', 'offset', 'opacity', 'orient', 'origin', + 'overline-position', 'overline-thickness', 'panose-1', 'path', + 'pathLength', 'points', 'preserveAspectRatio', 'r', 'refX', 'refY', + 'repeatCount', 'repeatDur', 'requiredExtensions', 'requiredFeatures', + 'restart', 'rotate', 'rx', 'ry', 'slope', 'stemh', 'stemv', + 'stop-color', 'stop-opacity', 'strikethrough-position', + 'strikethrough-thickness', 'stroke', 'stroke-dasharray', + 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', + 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', + 'target', 'text-anchor', 'to', 'transform', 'type', 'u1', 'u2', + 'underline-position', 'underline-thickness', 'unicode', 'unicode-range', + 'units-per-em', 'values', 'version', 'viewBox', 'visibility', 'width', + 'widths', 'x', 'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole', + 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type', + 'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y', 'y1', + 'y2', 'zoomAndPan'] + + svg_attr_map = None + svg_elem_map = None + + acceptable_svg_properties = [ 'fill', 'fill-opacity', 'fill-rule', + 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', + 'stroke-opacity'] + + def reset(self): + _BaseHTMLProcessor.reset(self) + self.unacceptablestack = 0 + self.mathmlOK = 0 + self.svgOK = 0 + + def unknown_starttag(self, tag, attrs): + acceptable_attributes = self.acceptable_attributes + keymap = {} + if not tag in self.acceptable_elements or self.svgOK: + if tag in self.unacceptable_elements_with_end_tag: + self.unacceptablestack += 1 + + # not otherwise acceptable, perhaps it is MathML or SVG? + if tag=='math' and ('xmlns','http://www.w3.org/1998/Math/MathML') in attrs: + self.mathmlOK += 1 + if tag=='svg' and ('xmlns','http://www.w3.org/2000/svg') in attrs: + self.svgOK += 1 + + # chose acceptable attributes based on tag class, else bail + if self.mathmlOK and tag in self.mathml_elements: + acceptable_attributes = self.mathml_attributes + elif self.svgOK and tag in self.svg_elements: + # for most vocabularies, lowercasing is a good idea. Many + # svg elements, however, are camel case + if not self.svg_attr_map: + lower=[attr.lower() for attr in self.svg_attributes] + mix=[a for a in self.svg_attributes if a not in lower] + self.svg_attributes = lower + self.svg_attr_map = dict([(a.lower(),a) for a in mix]) + + lower=[attr.lower() for attr in self.svg_elements] + mix=[a for a in self.svg_elements if a not in lower] + self.svg_elements = lower + self.svg_elem_map = dict([(a.lower(),a) for a in mix]) + acceptable_attributes = self.svg_attributes + tag = self.svg_elem_map.get(tag,tag) + keymap = self.svg_attr_map + elif not tag in self.acceptable_elements: + return + + # declare xlink namespace, if needed + if self.mathmlOK or self.svgOK: + if filter(lambda (n,v): n.startswith('xlink:'),attrs): + if not ('xmlns:xlink','http://www.w3.org/1999/xlink') in attrs: + attrs.append(('xmlns:xlink','http://www.w3.org/1999/xlink')) + + clean_attrs = [] + for key, value in self.normalize_attrs(attrs): + if key in acceptable_attributes: + key=keymap.get(key,key) + clean_attrs.append((key,value)) + elif key=='style': + pass + ## clean_value = self.sanitize_style(value) + ## if clean_value: clean_attrs.append((key,clean_value)) + _BaseHTMLProcessor.unknown_starttag(self, tag, clean_attrs) + + def unknown_endtag(self, tag): + if not tag in self.acceptable_elements: + if tag in self.unacceptable_elements_with_end_tag: + self.unacceptablestack -= 1 + if self.mathmlOK and tag in self.mathml_elements: + if tag == 'math' and self.mathmlOK: self.mathmlOK -= 1 + elif self.svgOK and tag in self.svg_elements: + tag = self.svg_elem_map.get(tag,tag) + if tag == 'svg' and self.svgOK: self.svgOK -= 1 + else: + return + _BaseHTMLProcessor.unknown_endtag(self, tag) + + def handle_pi(self, text): + pass + + def handle_decl(self, text): + pass + + def handle_data(self, text): + if not self.unacceptablestack: + _BaseHTMLProcessor.handle_data(self, text) + + def sanitize_style(self, style): + # disallow urls + style=re.compile('url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ',style) + + # gauntlet + if not re.match("""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): return '' + if not re.match("^(\s*[-\w]+\s*:\s*[^:;]*(;|$))*$", style): return '' + + clean = [] + for prop,value in re.findall("([-\w]+)\s*:\s*([^:;]*)",style): + if not value: continue + if prop.lower() in self.acceptable_css_properties: + clean.append(prop + ': ' + value + ';') + elif prop.split('-')[0].lower() in ['background','border','margin','padding']: + for keyword in value.split(): + if not keyword in self.acceptable_css_keywords and \ + not self.valid_css_values.match(keyword): + break + else: + clean.append(prop + ': ' + value + ';') + elif self.svgOK and prop.lower() in self.acceptable_svg_properties: + clean.append(prop + ': ' + value + ';') + + return ' '.join(clean) + + +def sanitize_html(htmlSource, encoding, type): + p = _HTMLSanitizer(encoding, type) + p.feed(htmlSource) + data = p.output() + data = data.strip().replace('\r\n', '\n') + return data \ No newline at end of file diff -r 4ff4be2f436f -r 9c325f3656b5 lib/galaxy/web/__init__.py --- a/lib/galaxy/web/__init__.py Mon Aug 24 15:27:27 2009 -0400 +++ b/lib/galaxy/web/__init__.py Mon Aug 24 15:53:17 2009 -0400 @@ -3,4 +3,5 @@ """ from framework import expose, json, require_login, require_admin, url_for, error, form, FormBuilder +from framework.base import httpexceptions diff -r 4ff4be2f436f -r 9c325f3656b5 lib/galaxy/web/buildapp.py --- a/lib/galaxy/web/buildapp.py Mon Aug 24 15:27:27 2009 -0400 +++ b/lib/galaxy/web/buildapp.py Mon Aug 24 15:53:17 2009 -0400 @@ -74,6 +74,7 @@ webapp.add_route( '/:controller/:action', action='index' ) webapp.add_route( '/:action', controller='root', action='index' ) webapp.add_route( '/datasets/:dataset_id/:action/:filename', controller='dataset', action='index', dataset_id=None, filename=None) + webapp.add_route( '/u/:username/p/:slug', controller='page', action='display_by_username_and_slug' ) webapp.finalize_config() # Wrap the webapp in some useful middleware if kwargs.get( 'middleware', True ): diff -r 4ff4be2f436f -r 9c325f3656b5 lib/galaxy/web/controllers/page.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/web/controllers/page.py Mon Aug 24 15:53:17 2009 -0400 @@ -0,0 +1,142 @@ +from galaxy.web.base.controller import * +from galaxy.web.framework.helpers import * +from galaxy.util.sanitize_html import sanitize_html + +import re + +VALID_SLUG_RE = re.compile( "^[a-z0-9\-]+$" ) + +class PageListGrid( grids.Grid ): + class URLColumn( grids.GridColumn ): + def get_value( self, trans, grid, item ): + username = trans.user.username or "???" + return username + "/" + item.slug + def get_link( self, trans, grid, item ): + if trans.user.username: + return dict( action='display_by_username_and_slug', username=trans.user.username, slug=item.slug ) + else: + return None + # Grid definition + use_panels = True + title = "Your pages" + model_class = model.Page + default_sort_key = "-create_time" + columns = [ + grids.GridColumn( "Title", key="title", attach_popup=True ), + URLColumn( "Public URL" ), + grids.GridColumn( "Created", key="create_time", format=time_ago ), + grids.GridColumn( "Last Updated", key="update_time", format=time_ago ), + ] + global_actions = [ + grids.GridAction( "Add new page", dict( action='create' ) ) + ] + operations = [ + grids.GridOperation( "View", allow_multiple=False, url_args=dict( action='display') ), + grids.GridOperation( "Edit", allow_multiple=False, url_args=dict( action='edit') ) + ] + +class PageController( BaseController ): + + list = PageListGrid() + + @web.expose + @web.require_admin + def index( self, trans, *args, **kwargs ): + grid = self.list( trans, *args, **kwargs ) + return trans.fill_template( "page/index.mako", grid=grid ) + + @web.expose + @web.require_admin + @web.require_login( "create pages" ) + def create( self, trans, page_title="", page_slug="" ): + """ + Create a new page + """ + user = trans.get_user() + page_title_err = page_slug_err = "" + if trans.request.method == "POST": + if not page_title: + page_title_err = "Page name is required" + elif not page_slug: + page_slug_err = "Page id is required" + elif not VALID_SLUG_RE.match( page_slug ): + page_slug_err = "Page identifier must consist of only lowercase letters, numbers, and the '-' character" + elif model.Page.filter_by( user=user, slug=page_slug ).first(): + page_slug_err = "Page id must be unique" + else: + # Create the new stored workflow + page = model.Page() + page.title = page_title + page.slug = page_slug + page.user = user + # And the first (empty) workflow revision + page_revision = model.PageRevision() + page_revision.title = page_title + page_revision.page = page + page.latest_revision = page_revision + page_revision.content = "" + # Persist + session = trans.sa_session + session.save_or_update( page ) + session.flush() + # Display the management page + trans.set_message( "Page '%s' created" % page.title ) + return self.list( trans ) + return trans.show_form( + web.FormBuilder( web.url_for(), "Create new page", submit_text="Submit" ) + .add_text( "page_title", "Page title", value=page_title, error=page_title_err ) + .add_text( "page_slug", "Page identifier", value=page_slug, error=page_slug_err, + help="""A unique identifier that will be used for + public links to this page. A default is generated + from the page title, but can be edited. This field + must contain only lowercase letters, numbers, and + the '-' character.""" ), + template="page/create.mako" ) + + @web.expose + @web.require_admin + @web.require_login( "edit pages" ) + def edit( self, trans, id ): + """ + Render the main page editor interface. + """ + id = trans.security.decode_id( id ) + page = trans.sa_session.query( model.Page ).get( id ) + assert page.user == trans.user + return trans.fill_template( "page/editor.mako", page=page ) + + @web.expose + @web.require_admin + def save( self, trans, id, content ): + id = trans.security.decode_id( id ) + page = trans.sa_session.query( model.Page ).get( id ) + assert page.user == trans.user + # Sanitize content + content = sanitize_html( content, 'utf-8', 'text/html' ) + # Add a new revision to the page with the provided content + page_revision = model.PageRevision() + page_revision.title = page.title + page_revision.page = page + page.latest_revision = page_revision + page_revision.content = content + trans.sa_session.flush() + + @web.expose + @web.require_admin + def display( self, trans, id ): + id = trans.security.decode_id( id ) + page = trans.sa_session.query( model.Page ).get( id ) + return trans.fill_template( "page/display.mako", page=page ) + + @web.expose + def display_by_username_and_slug( self, trans, username, slug ): + session = trans.sa_session + user = session.query( model.User ).filter_by( username=username ).first() + if user is None: + raise web.httpexceptions.HTTPNotFound() + page = trans.sa_session.query( model.Page ).filter_by( user=user, slug=slug ).first() + if page is None: + raise web.httpexceptions.HTTPNotFound() + return trans.fill_template( "page/display.mako", page=page ) + + \ No newline at end of file diff -r 4ff4be2f436f -r 9c325f3656b5 lib/galaxy/web/controllers/user.py --- a/lib/galaxy/web/controllers/user.py Mon Aug 24 15:27:27 2009 -0400 +++ b/lib/galaxy/web/controllers/user.py Mon Aug 24 15:53:17 2009 -0400 @@ -4,7 +4,7 @@ from galaxy.web.base.controller import * from galaxy.model.orm import * from galaxy import util -import logging, os, string +import logging, os, string, re from random import choice log = logging.getLogger( __name__ ) @@ -19,6 +19,8 @@ """ require_login_nocreation_template = require_login_template % "" require_login_creation_template = require_login_template % " If you don't already have an account, <a href='%s'>you may create one</a>." + +VALID_USERNAME_RE = re.compile( "^[a-z0-9\-]+$" ) class User( BaseController ): edit_address_id = None @@ -78,6 +80,37 @@ .add_text( "email", "Email", value=email, error=email_err ) .add_text( "conf_email", "Confirm Email", value='', error=conf_email_err ) .add_password( "password", "Password", value='', error=pass_err ) ) + + @web.expose + def change_username(self, trans, username='', **kwd): + username_err = '' + user = trans.get_user() + if not user: + trans.response.send_redirect( web.url_for( action='login' ) ) + if trans.request.method == "POST": + if len( username ) < 4: + username_err = "Username must be at least 4 characters in length" + elif len( username ) > 255: + username_err = "USername must be at most 255 characters in length" + elif not( VALID_USERNAME_RE.match( username ) ): + username_err = "Username must contain only letters, numbers, '-', and '_'" + elif trans.app.model.User.filter_by( username=username ).first(): + username_err = "This username is not available" + else: + user.username = username + user.flush() + trans.log_event( "User change username" ) + return trans.show_ok_message( "Username been set to: " + user.username ) + else: + username = user.username or '' + return trans.show_form( + web.FormBuilder( web.url_for(), "Change username", submit_text="Submit" ) + .add_text( "username", "Username", value=username, error=username_err, + help="""Your username is an optional identifier that + will be used to generate adresses for information + you share publicly. Usernames must be at least + four characters in length and contain only lowercase + letters, numbers, and the '-' character.""" ) ) @web.expose def login( self, trans, email='', password='' ): diff -r 4ff4be2f436f -r 9c325f3656b5 lib/galaxy/web/framework/helpers/grids.py --- a/lib/galaxy/web/framework/helpers/grids.py Mon Aug 24 15:27:27 2009 -0400 +++ b/lib/galaxy/web/framework/helpers/grids.py Mon Aug 24 15:53:17 2009 -0400 @@ -15,6 +15,7 @@ exposed = True model_class = None template = "grid.mako" + global_actions = [] columns = [] operations = [] standard_filters = [] @@ -22,7 +23,12 @@ default_sort_key = None pass_through_operations = {} def __init__( self ): - pass + # Determine if any multiple row operations are defined + self.has_multiple_item_operations = False + for operation in self.operations: + if operation.allow_multiple: + self.has_multiple_item_operations = True + break def __call__( self, trans, **kwargs ): status = kwargs.get( 'status', None ) message = kwargs.get( 'message', None ) @@ -65,7 +71,13 @@ current_item = self.get_current_item( trans ) # Render def url( *args, **kwargs ): - new_kwargs = dict( extra_url_args ) + # Only include sort/filter arguments if not linking to another + # page. This is a bit of a hack. + if 'action' in kwargs: + new_kwargs = dict() + else: + new_kwargs = dict( extra_url_args ) + # Extend new_kwargs with first argument if found if len(args) > 0: new_kwargs.update( args[0] ) new_kwargs.update( kwargs ) @@ -158,16 +170,31 @@ return query class GridOperation( object ): - def __init__( self, label, key=None, condition=None, allow_multiple=True ): + def __init__( self, label, key=None, condition=None, allow_multiple=True, target=None, url_args=None ): self.label = label self.key = key self.allow_multiple = allow_multiple self.condition = condition + self.target = target + self.url_args = url_args + def get_url_args( self, item ): + if self.url_args: + temp = dict( self.url_args ) + temp['id'] = item.id + return temp + else: + return dict( operation=operation.label, id=item.id ) + def allowed( self, item ): if self.condition: return self.condition( item ) else: return True + +class GridAction( object ): + def __init__( self, label=None, url_args=None ): + self.label = label + self.url_args = url_args class GridColumnFilter( object ): def __init__( self, label, args=None ): diff -r 4ff4be2f436f -r 9c325f3656b5 static/scripts/jquery.wymeditor.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/static/scripts/jquery.wymeditor.js Mon Aug 24 15:53:17 2009 -0400 @@ -0,0 +1,4707 @@ +/** + * @version 0.5-rc1 + * + * WYMeditor : what you see is What You Mean web-based editor + * Copyright (c) 2005 - 2009 Jean-Francois Hovinne, http://www.wymeditor.org/ + * Dual licensed under the MIT (MIT-license.txt) + * and GPL (GPL-license.txt) licenses. + * + * For further information visit: + * http://www.wymeditor.org/ + * + * File: jquery.wymeditor.js + * + * Main JS file with core classes and functions. + * See the documentation for more info. + * + * About: authors + * + * Jean-Francois Hovinne (jf.hovinne a-t wymeditor dotorg) + * Volker Mische (vmx a-t gmx dotde) + * Scott Lewis (lewiscot a-t gmail dotcom) + * Bermi Ferrer (wymeditor a-t bermi dotorg) + * Daniel Reszka (d.reszka a-t wymeditor dotorg) + * Jonatan Lundin (jonatan.lundin _at_ gmail.com) + */ + +/* + Namespace: WYMeditor + Global WYMeditor namespace. +*/ +if(!WYMeditor) var WYMeditor = {}; + +//Wrap the Firebug console in WYMeditor.console +(function() { + if ( !window.console || !console.firebug ) { + var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", + "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"]; + + WYMeditor.console = {}; + for (var i = 0; i < names.length; ++i) + WYMeditor.console[names[i]] = function() {} + + } else WYMeditor.console = window.console; +})(); + +jQuery.extend(WYMeditor, { + +/* + Constants: Global WYMeditor constants. + + VERSION - Defines WYMeditor version. + INSTANCES - An array of loaded WYMeditor.editor instances. + STRINGS - An array of loaded WYMeditor language pairs/values. + SKINS - An array of loaded WYMeditor skins. + NAME - The "name" attribute. + INDEX - A string replaced by the instance index. + WYM_INDEX - A string used to get/set the instance index. + BASE_PATH - A string replaced by WYMeditor's base path. + SKIN_PATH - A string replaced by WYMeditor's skin path. + WYM_PATH - A string replaced by WYMeditor's main JS file path. + SKINS_DEFAULT_PATH - The skins default base path. + SKINS_DEFAULT_CSS - The skins default CSS file. + LANG_DEFAULT_PATH - The language files default path. + IFRAME_BASE_PATH - A string replaced by the designmode iframe's base path. + IFRAME_DEFAULT - The iframe's default base path. + JQUERY_PATH - A string replaced by the computed jQuery path. + DIRECTION - A string replaced by the text direction (rtl or ltr). + LOGO - A string replaced by WYMeditor logo. + TOOLS - A string replaced by the toolbar's HTML. + TOOLS_ITEMS - A string replaced by the toolbar items. + TOOL_NAME - A string replaced by a toolbar item's name. + TOOL_TITLE - A string replaced by a toolbar item's title. + TOOL_CLASS - A string replaced by a toolbar item's class. + CLASSES - A string replaced by the classes panel's HTML. + CLASSES_ITEMS - A string replaced by the classes items. + CLASS_NAME - A string replaced by a class item's name. + CLASS_TITLE - A string replaced by a class item's title. + CONTAINERS - A string replaced by the containers panel's HTML. + CONTAINERS_ITEMS - A string replaced by the containers items. + CONTAINER_NAME - A string replaced by a container item's name. + CONTAINER_TITLE - A string replaced by a container item's title. + CONTAINER_CLASS - A string replaced by a container item's class. + HTML - A string replaced by the HTML view panel's HTML. + IFRAME - A string replaced by the designmode iframe. + STATUS - A string replaced by the status panel's HTML. + DIALOG_TITLE - A string replaced by a dialog's title. + DIALOG_BODY - A string replaced by a dialog's HTML body. + BODY - The BODY element. + STRING - The "string" type. + BODY,DIV,P, + H1,H2,H3,H4,H5,H6, + PRE,BLOCKQUOTE, + A,BR,IMG, + TABLE,TD,TH, + UL,OL,LI - HTML elements string representation. + CLASS,HREF,SRC, + TITLE,ALT - HTML attributes string representation. + DIALOG_LINK - A link dialog type. + DIALOG_IMAGE - An image dialog type. + DIALOG_TABLE - A table dialog type. + DIALOG_PASTE - A 'Paste from Word' dialog type. + BOLD - Command: (un)set selection to <strong>. + ITALIC - Command: (un)set selection to <em>. + CREATE_LINK - Command: open the link dialog or (un)set link. + INSERT_IMAGE - Command: open the image dialog or insert an image. + INSERT_TABLE - Command: open the table dialog. + PASTE - Command: open the paste dialog. + INDENT - Command: nest a list item. + OUTDENT - Command: unnest a list item. + TOGGLE_HTML - Command: display/hide the HTML view. + FORMAT_BLOCK - Command: set a block element to another type. + PREVIEW - Command: open the preview dialog. + UNLINK - Command: unset a link. + INSERT_UNORDEREDLIST- Command: insert an unordered list. + INSERT_ORDEREDLIST - Command: insert an ordered list. + MAIN_CONTAINERS - An array of the main HTML containers used in WYMeditor. + BLOCKS - An array of the HTML block elements. + KEY - Standard key codes. + NODE - Node types. + +*/ + + VERSION : "0.5-rc1", + INSTANCES : [], + STRINGS : [], + SKINS : [], + NAME : "name", + INDEX : "{Wym_Index}", + WYM_INDEX : "wym_index", + BASE_PATH : "{Wym_Base_Path}", + CSS_PATH : "{Wym_Css_Path}", + WYM_PATH : "{Wym_Wym_Path}", + SKINS_DEFAULT_PATH : "skins/", + SKINS_DEFAULT_CSS : "skin.css", + SKINS_DEFAULT_JS : "skin.js", + LANG_DEFAULT_PATH : "lang/", + IFRAME_BASE_PATH : "{Wym_Iframe_Base_Path}", + IFRAME_DEFAULT : "iframe/default/", + JQUERY_PATH : "{Wym_Jquery_Path}", + DIRECTION : "{Wym_Direction}", + LOGO : "{Wym_Logo}", + TOOLS : "{Wym_Tools}", + TOOLS_ITEMS : "{Wym_Tools_Items}", + TOOL_NAME : "{Wym_Tool_Name}", + TOOL_TITLE : "{Wym_Tool_Title}", + TOOL_CLASS : "{Wym_Tool_Class}", + CLASSES : "{Wym_Classes}", + CLASSES_ITEMS : "{Wym_Classes_Items}", + CLASS_NAME : "{Wym_Class_Name}", + CLASS_TITLE : "{Wym_Class_Title}", + CONTAINERS : "{Wym_Containers}", + CONTAINERS_ITEMS : "{Wym_Containers_Items}", + CONTAINER_NAME : "{Wym_Container_Name}", + CONTAINER_TITLE : "{Wym_Containers_Title}", + CONTAINER_CLASS : "{Wym_Container_Class}", + HTML : "{Wym_Html}", + IFRAME : "{Wym_Iframe}", + STATUS : "{Wym_Status}", + DIALOG_TITLE : "{Wym_Dialog_Title}", + DIALOG_BODY : "{Wym_Dialog_Body}", + STRING : "string", + BODY : "body", + DIV : "div", + P : "p", + H1 : "h1", + H2 : "h2", + H3 : "h3", + H4 : "h4", + H5 : "h5", + H6 : "h6", + PRE : "pre", + BLOCKQUOTE : "blockquote", + A : "a", + BR : "br", + IMG : "img", + TABLE : "table", + TD : "td", + TH : "th", + UL : "ul", + OL : "ol", + LI : "li", + CLASS : "class", + HREF : "href", + SRC : "src", + TITLE : "title", + ALT : "alt", + DIALOG_LINK : "Link", + DIALOG_IMAGE : "Image", + DIALOG_TABLE : "Table", + DIALOG_PASTE : "Paste_From_Word", + BOLD : "Bold", + ITALIC : "Italic", + CREATE_LINK : "CreateLink", + INSERT_IMAGE : "InsertImage", + INSERT_TABLE : "InsertTable", + INSERT_HTML : "InsertHTML", + PASTE : "Paste", + INDENT : "Indent", + OUTDENT : "Outdent", + TOGGLE_HTML : "ToggleHtml", + FORMAT_BLOCK : "FormatBlock", + PREVIEW : "Preview", + UNLINK : "Unlink", + INSERT_UNORDEREDLIST: "InsertUnorderedList", + INSERT_ORDEREDLIST : "InsertOrderedList", + + MAIN_CONTAINERS : new Array("p","h1","h2","h3","h4","h5","h6","pre","blockquote"), + + BLOCKS : new Array("address", "blockquote", "div", "dl", + "fieldset", "form", "h1", "h2", "h3", "h4", "h5", "h6", "hr", + "noscript", "ol", "p", "pre", "table", "ul", "dd", "dt", + "li", "tbody", "td", "tfoot", "th", "thead", "tr"), + + KEY : { + BACKSPACE: 8, + ENTER: 13, + END: 35, + HOME: 36, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + CURSOR: new Array(37, 38, 39, 40), + DELETE: 46 + }, + + NODE : { + ELEMENT: 1, + ATTRIBUTE: 2, + TEXT: 3 + }, + + /* + Class: WYMeditor.editor + WYMeditor editor main class, instanciated for each editor occurrence. + */ + + editor : function(elem, options) { + + /* + Constructor: WYMeditor.editor + + Initializes main values (index, elements, paths, ...) + and call WYMeditor.editor.init which initializes the editor. + + Parameters: + + elem - The HTML element to be replaced by the editor. + options - The hash of options. + + Returns: + + Nothing. + + See Also: + + <WYMeditor.editor.init> + */ + + //store the instance in the INSTANCES array and store the index + this._index = WYMeditor.INSTANCES.push(this) - 1; + //store the element replaced by the editor + this._element = elem; + //store the options + this._options = options; + //store the element's inner value + this._html = jQuery(elem).val(); + + //store the HTML option, if any + if(this._options.html) this._html = this._options.html; + //get or compute the base path (where the main JS file is located) + this._options.basePath = this._options.basePath + || this.computeBasePath(); + //get or set the skin path (where the skin files are located) + this._options.skinPath = this._options.skinPath + || this._options.basePath + WYMeditor.SKINS_DEFAULT_PATH + + this._options.skin + '/'; + //get or compute the main JS file location + this._options.wymPath = this._options.wymPath + || this.computeWymPath(); + //get or set the language files path + this._options.langPath = this._options.langPath + || this._options.basePath + WYMeditor.LANG_DEFAULT_PATH; + //get or set the designmode iframe's base path + this._options.iframeBasePath = this._options.iframeBasePath + || this._options.basePath + WYMeditor.IFRAME_DEFAULT; + //get or compute the jQuery JS file location + this._options.jQueryPath = this._options.jQueryPath + || this.computeJqueryPath(); + + //initialize the editor instance + this.init(); + + } + +}); + + +/********** JQUERY **********/ + +/** + * Replace an HTML element by WYMeditor + * + * @example jQuery(".wymeditor").wymeditor( + * { + * + * } + * ); + * @desc Example description here + * + * @name WYMeditor + * @description WYMeditor is a web-based WYSIWYM XHTML editor + * @param Hash hash A hash of parameters + * @option Integer iExample Description here + * @option String sExample Description here + * + * @type jQuery + * @cat Plugins/WYMeditor + * @author Jean-Francois Hovinne + */ +jQuery.fn.wymeditor = function(options) { + + options = jQuery.extend({ + + html: "", + + basePath: false, + + skinPath: false, + + wymPath: false, + + iframeBasePath: false, + + jQueryPath: false, + + styles: false, + + stylesheet: false, + + skin: "default", + initSkin: true, + loadSkin: true, + + lang: "en", + + direction: "ltr", + + boxHtml: "<div class='wym_box'>" + + "<div class='wym_area_top'>" + + WYMeditor.TOOLS + + "</div>" + + "<div class='wym_area_left'></div>" + + "<div class='wym_area_right'>" + + WYMeditor.CONTAINERS + + WYMeditor.CLASSES + + "</div>" + + "<div class='wym_area_main'>" + + WYMeditor.HTML + + WYMeditor.IFRAME + + WYMeditor.STATUS + + "</div>" + + "<div class='wym_area_bottom'>" + + WYMeditor.LOGO + + "</div>" + + "</div>", + + logoHtml: "<a class='wym_wymeditor_link' " + + "href='http://www.wymeditor.org/'>WYMeditor</a>", + + iframeHtml:"<div class='wym_iframe wym_section'>" + + "<iframe " + + "src='" + + WYMeditor.IFRAME_BASE_PATH + + "wymiframe.html' " + + "onload='this.contentWindow.parent.WYMeditor.INSTANCES[" + + WYMeditor.INDEX + "].initIframe(this)'" + + "></iframe>" + + "</div>", + + editorStyles: [], + + toolsHtml: "<div class='wym_tools wym_section'>" + + "<h2>{Tools}</h2>" + + "<ul>" + + WYMeditor.TOOLS_ITEMS + + "</ul>" + + "</div>", + + toolsItemHtml: "<li class='" + + WYMeditor.TOOL_CLASS + + "'><a href='#' name='" + + WYMeditor.TOOL_NAME + + "' title='" + + WYMeditor.TOOL_TITLE + + "'>" + + WYMeditor.TOOL_TITLE + + "</a></li>", + + toolsItems: [ + {'name': 'Bold', 'title': 'Strong', 'css': 'wym_tools_strong'}, + {'name': 'Italic', 'title': 'Emphasis', 'css': 'wym_tools_emphasis'}, + {'name': 'Superscript', 'title': 'Superscript', + 'css': 'wym_tools_superscript'}, + {'name': 'Subscript', 'title': 'Subscript', + 'css': 'wym_tools_subscript'}, + {'name': 'InsertOrderedList', 'title': 'Ordered_List', + 'css': 'wym_tools_ordered_list'}, + {'name': 'InsertUnorderedList', 'title': 'Unordered_List', + 'css': 'wym_tools_unordered_list'}, + {'name': 'Indent', 'title': 'Indent', 'css': 'wym_tools_indent'}, + {'name': 'Outdent', 'title': 'Outdent', 'css': 'wym_tools_outdent'}, + {'name': 'Undo', 'title': 'Undo', 'css': 'wym_tools_undo'}, + {'name': 'Redo', 'title': 'Redo', 'css': 'wym_tools_redo'}, + {'name': 'CreateLink', 'title': 'Link', 'css': 'wym_tools_link'}, + {'name': 'Unlink', 'title': 'Unlink', 'css': 'wym_tools_unlink'}, + {'name': 'InsertImage', 'title': 'Image', 'css': 'wym_tools_image'}, + {'name': 'InsertTable', 'title': 'Table', 'css': 'wym_tools_table'}, + {'name': 'Paste', 'title': 'Paste_From_Word', + 'css': 'wym_tools_paste'}, + {'name': 'ToggleHtml', 'title': 'HTML', 'css': 'wym_tools_html'}, + {'name': 'Preview', 'title': 'Preview', 'css': 'wym_tools_preview'} + ], + + containersHtml: "<div class='wym_containers wym_section'>" + + "<h2>{Containers}</h2>" + + "<ul>" + + WYMeditor.CONTAINERS_ITEMS + + "</ul>" + + "</div>", + + containersItemHtml:"<li class='" + + WYMeditor.CONTAINER_CLASS + + "'>" + + "<a href='#' name='" + + WYMeditor.CONTAINER_NAME + + "'>" + + WYMeditor.CONTAINER_TITLE + + "</a></li>", + + containersItems: [ + {'name': 'P', 'title': 'Paragraph', 'css': 'wym_containers_p'}, + {'name': 'H1', 'title': 'Heading_1', 'css': 'wym_containers_h1'}, + {'name': 'H2', 'title': 'Heading_2', 'css': 'wym_containers_h2'}, + {'name': 'H3', 'title': 'Heading_3', 'css': 'wym_containers_h3'}, + {'name': 'H4', 'title': 'Heading_4', 'css': 'wym_containers_h4'}, + {'name': 'H5', 'title': 'Heading_5', 'css': 'wym_containers_h5'}, + {'name': 'H6', 'title': 'Heading_6', 'css': 'wym_containers_h6'}, + {'name': 'PRE', 'title': 'Preformatted', 'css': 'wym_containers_pre'}, + {'name': 'BLOCKQUOTE', 'title': 'Blockquote', + 'css': 'wym_containers_blockquote'}, + {'name': 'TH', 'title': 'Table_Header', 'css': 'wym_containers_th'} + ], + + classesHtml: "<div class='wym_classes wym_section'>" + + "<h2>{Classes}</h2><ul>" + + WYMeditor.CLASSES_ITEMS + + "</ul></div>", + + classesItemHtml: "<li><a href='#' name='" + + WYMeditor.CLASS_NAME + + "'>" + + WYMeditor.CLASS_TITLE + + "</a></li>", + + classesItems: [], + + statusHtml: "<div class='wym_status wym_section'>" + + "<h2>{Status}</h2>" + + "</div>", + + htmlHtml: "<div class='wym_html wym_section'>" + + "<h2>{Source_Code}</h2>" + + "<textarea class='wym_html_val'></textarea>" + + "</div>", + + boxSelector: ".wym_box", + toolsSelector: ".wym_tools", + toolsListSelector: " ul", + containersSelector:".wym_containers", + classesSelector: ".wym_classes", + htmlSelector: ".wym_html", + iframeSelector: ".wym_iframe iframe", + iframeBodySelector:".wym_iframe", + statusSelector: ".wym_status", + toolSelector: ".wym_tools a", + containerSelector: ".wym_containers a", + classSelector: ".wym_classes a", + htmlValSelector: ".wym_html_val", + + hrefSelector: ".wym_href", + srcSelector: ".wym_src", + titleSelector: ".wym_title", + altSelector: ".wym_alt", + textSelector: ".wym_text", + + rowsSelector: ".wym_rows", + colsSelector: ".wym_cols", + captionSelector: ".wym_caption", + summarySelector: ".wym_summary", + + submitSelector: ".wym_submit", + cancelSelector: ".wym_cancel", + previewSelector: "", + + dialogTypeSelector: ".wym_dialog_type", + dialogLinkSelector: ".wym_dialog_link", + dialogImageSelector: ".wym_dialog_image", + dialogTableSelector: ".wym_dialog_table", + dialogPasteSelector: ".wym_dialog_paste", + dialogPreviewSelector: ".wym_dialog_preview", + + updateSelector: ".wymupdate", + updateEvent: "click", + + dialogFeatures: "menubar=no,titlebar=no,toolbar=no,resizable=no" + + ",width=560,height=300,top=0,left=0", + dialogFeaturesPreview: "menubar=no,titlebar=no,toolbar=no,resizable=no" + + ",scrollbars=yes,width=560,height=300,top=0,left=0", + + dialogHtml: "<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Strict//EN'" + + " 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'>" + + "<html dir='" + + WYMeditor.DIRECTION + + "'><head>" + + "<link rel='stylesheet' type='text/css' media='screen'" + + " href='" + + WYMeditor.CSS_PATH + + "' />" + + "<title>" + + WYMeditor.DIALOG_TITLE + + "</title>" + + "<script type='text/javascript'" + + " src='" + + WYMeditor.JQUERY_PATH + + "'></script>" + + "<script type='text/javascript'" + + " src='" + + WYMeditor.WYM_PATH + + "'></script>" + + "</head>" + + WYMeditor.DIALOG_BODY + + "</html>", + + dialogLinkHtml: "<body class='wym_dialog wym_dialog_link'" + + " onload='WYMeditor.INIT_DIALOG(" + WYMeditor.INDEX + ")'" + + ">" + + "<form>" + + "<fieldset>" + + "<input type='hidden' class='wym_dialog_type' value='" + + WYMeditor.DIALOG_LINK + + "' />" + + "<legend>{Link}</legend>" + + "<div class='row'>" + + "<label>{URL}</label>" + + "<input type='text' class='wym_href' value='' size='40' />" + + "</div>" + + "<div class='row'>" + + "<label>{Title}</label>" + + "<input type='text' class='wym_title' value='' size='40' />" + + "</div>" + + "<div class='row row-indent'>" + + "<input class='wym_submit' type='button'" + + " value='{Submit}' />" + + "<input class='wym_cancel' type='button'" + + "value='{Cancel}' />" + + "</div>" + + "</fieldset>" + + "</form>" + + "</body>", + + dialogImageHtml: "<body class='wym_dialog wym_dialog_image'" + + " onload='WYMeditor.INIT_DIALOG(" + WYMeditor.INDEX + ")'" + + ">" + + "<form>" + + "<fieldset>" + + "<input type='hidden' class='wym_dialog_type' value='" + + WYMeditor.DIALOG_IMAGE + + "' />" + + "<legend>{Image}</legend>" + + "<div class='row'>" + + "<label>{URL}</label>" + + "<input type='text' class='wym_src' value='' size='40' />" + + "</div>" + + "<div class='row'>" + + "<label>{Alternative_Text}</label>" + + "<input type='text' class='wym_alt' value='' size='40' />" + + "</div>" + + "<div class='row'>" + + "<label>{Title}</label>" + + "<input type='text' class='wym_title' value='' size='40' />" + + "</div>" + + "<div class='row row-indent'>" + + "<input class='wym_submit' type='button'" + + " value='{Submit}' />" + + "<input class='wym_cancel' type='button'" + + "value='{Cancel}' />" + + "</div>" + + "</fieldset>" + + "</form>" + + "</body>", + + dialogTableHtml: "<body class='wym_dialog wym_dialog_table'" + + " onload='WYMeditor.INIT_DIALOG(" + WYMeditor.INDEX + ")'" + + ">" + + "<form>" + + "<fieldset>" + + "<input type='hidden' class='wym_dialog_type' value='" + + WYMeditor.DIALOG_TABLE + + "' />" + + "<legend>{Table}</legend>" + + "<div class='row'>" + + "<label>{Caption}</label>" + + "<input type='text' class='wym_caption' value='' size='40' />" + + "</div>" + + "<div class='row'>" + + "<label>{Summary}</label>" + + "<input type='text' class='wym_summary' value='' size='40' />" + + "</div>" + + "<div class='row'>" + + "<label>{Number_Of_Rows}</label>" + + "<input type='text' class='wym_rows' value='3' size='3' />" + + "</div>" + + "<div class='row'>" + + "<label>{Number_Of_Cols}</label>" + + "<input type='text' class='wym_cols' value='2' size='3' />" + + "</div>" + + "<div class='row row-indent'>" + + "<input class='wym_submit' type='button'" + + " value='{Submit}' />" + + "<input class='wym_cancel' type='button'" + + "value='{Cancel}' />" + + "</div>" + + "</fieldset>" + + "</form>" + + "</body>", + + dialogPasteHtml: "<body class='wym_dialog wym_dialog_paste'" + + " onload='WYMeditor.INIT_DIALOG(" + WYMeditor.INDEX + ")'" + + ">" + + "<form>" + + "<input type='hidden' class='wym_dialog_type' value='" + + WYMeditor.DIALOG_PASTE + + "' />" + + "<fieldset>" + + "<legend>{Paste_From_Word}</legend>" + + "<div class='row'>" + + "<textarea class='wym_text' rows='10' cols='50'></textarea>" + + "</div>" + + "<div class='row'>" + + "<input class='wym_submit' type='button'" + + " value='{Submit}' />" + + "<input class='wym_cancel' type='button'" + + "value='{Cancel}' />" + + "</div>" + + "</fieldset>" + + "</form>" + + "</body>", + + dialogPreviewHtml: "<body class='wym_dialog wym_dialog_preview'" + + " onload='WYMeditor.INIT_DIALOG(" + WYMeditor.INDEX + ")'" + + "></body>", + + dialogStyles: [], + + stringDelimiterLeft: "{", + stringDelimiterRight:"}", + + preInit: null, + preBind: null, + postInit: null, + + preInitDialog: null, + postInitDialog: null + + }, options); + + return this.each(function() { + + new WYMeditor.editor(jQuery(this),options); + }); +}; + +/* @name extend + * @description Returns the WYMeditor instance based on its index + */ +jQuery.extend({ + wymeditors: function(i) { + return (WYMeditor.INSTANCES[i]); + } +}); + + +/********** WYMeditor **********/ + +/* @name Wymeditor + * @description WYMeditor class + */ + +/* @name init + * @description Initializes a WYMeditor instance + */ +WYMeditor.editor.prototype.init = function() { + + //load subclass - browser specific + //unsupported browsers: do nothing + if (jQuery.browser.msie) { + var WymClass = new WYMeditor.WymClassExplorer(this); + } + else if (jQuery.browser.mozilla) { + var WymClass = new WYMeditor.WymClassMozilla(this); + } + else if (jQuery.browser.opera) { + var WymClass = new WYMeditor.WymClassOpera(this); + } + else if (jQuery.browser.safari) { + var WymClass = new WYMeditor.WymClassSafari(this); + } + + if(WymClass) { + + if(jQuery.isFunction(this._options.preInit)) this._options.preInit(this); + + var SaxListener = new WYMeditor.XhtmlSaxListener(); + jQuery.extend(SaxListener, WymClass); + this.parser = new WYMeditor.XhtmlParser(SaxListener); + + if(this._options.styles || this._options.stylesheet){ + this.configureEditorUsingRawCss(); + } + + this.helper = new WYMeditor.XmlHelper(); + + //extend the Wymeditor object + //don't use jQuery.extend since 1.1.4 + //jQuery.extend(this, WymClass); + for (var prop in WymClass) { this[prop] = WymClass[prop]; } + + //load wymbox + this._box = jQuery(this._element).hide().after(this._options.boxHtml).next().addClass('wym_box_' + this._index); + + //store the instance index in wymbox and element replaced by editor instance + //but keep it compatible with jQuery < 1.2.3, see #122 + if( jQuery.isFunction( jQuery.fn.data ) ) { + jQuery.data(this._box.get(0), WYMeditor.WYM_INDEX, this._index); + jQuery.data(this._element.get(0), WYMeditor.WYM_INDEX, this._index); + } + + var h = WYMeditor.Helper; + + //construct the iframe + var iframeHtml = this._options.iframeHtml; + iframeHtml = h.replaceAll(iframeHtml, WYMeditor.INDEX, this._index); + iframeHtml = h.replaceAll(iframeHtml, WYMeditor.IFRAME_BASE_PATH, this._options.iframeBasePath); + + //construct wymbox + var boxHtml = jQuery(this._box).html(); + + boxHtml = h.replaceAll(boxHtml, WYMeditor.LOGO, this._options.logoHtml); + boxHtml = h.replaceAll(boxHtml, WYMeditor.TOOLS, this._options.toolsHtml); + boxHtml = h.replaceAll(boxHtml, WYMeditor.CONTAINERS,this._options.containersHtml); + boxHtml = h.replaceAll(boxHtml, WYMeditor.CLASSES, this._options.classesHtml); + boxHtml = h.replaceAll(boxHtml, WYMeditor.HTML, this._options.htmlHtml); + boxHtml = h.replaceAll(boxHtml, WYMeditor.IFRAME, iframeHtml); + boxHtml = h.replaceAll(boxHtml, WYMeditor.STATUS, this._options.statusHtml); + + //construct tools list + var aTools = eval(this._options.toolsItems); + var sTools = ""; + + for(var i = 0; i < aTools.length; i++) { + var oTool = aTools[i]; + if(oTool.name && oTool.title) + var sTool = this._options.toolsItemHtml; + var sTool = h.replaceAll(sTool, WYMeditor.TOOL_NAME, oTool.name); + sTool = h.replaceAll(sTool, WYMeditor.TOOL_TITLE, this._options.stringDelimiterLeft + + oTool.title + + this._options.stringDelimiterRight); + sTool = h.replaceAll(sTool, WYMeditor.TOOL_CLASS, oTool.css); + sTools += sTool; + } + + boxHtml = h.replaceAll(boxHtml, WYMeditor.TOOLS_ITEMS, sTools); + + //construct classes list + var aClasses = eval(this._options.classesItems); + var sClasses = ""; + + for(var i = 0; i < aClasses.length; i++) { + var oClass = aClasses[i]; + if(oClass.name && oClass.title) + var sClass = this._options.classesItemHtml; + sClass = h.replaceAll(sClass, WYMeditor.CLASS_NAME, oClass.name); + sClass = h.replaceAll(sClass, WYMeditor.CLASS_TITLE, oClass.title); + sClasses += sClass; + } + + boxHtml = h.replaceAll(boxHtml, WYMeditor.CLASSES_ITEMS, sClasses); + + //construct containers list + var aContainers = eval(this._options.containersItems); + var sContainers = ""; + + for(var i = 0; i < aContainers.length; i++) { + var oContainer = aContainers[i]; + if(oContainer.name && oContainer.title) + var sContainer = this._options.containersItemHtml; + sContainer = h.replaceAll(sContainer, WYMeditor.CONTAINER_NAME, oContainer.name); + sContainer = h.replaceAll(sContainer, WYMeditor.CONTAINER_TITLE, + this._options.stringDelimiterLeft + + oContainer.title + + this._options.stringDelimiterRight); + sContainer = h.replaceAll(sContainer, WYMeditor.CONTAINER_CLASS, oContainer.css); + sContainers += sContainer; + } + + boxHtml = h.replaceAll(boxHtml, WYMeditor.CONTAINERS_ITEMS, sContainers); + + //l10n + boxHtml = this.replaceStrings(boxHtml); + + //load html in wymbox + jQuery(this._box).html(boxHtml); + + //hide the html value + jQuery(this._box).find(this._options.htmlSelector).hide(); + + //enable the skin + this.loadSkin(); + + } +}; + +WYMeditor.editor.prototype.bindEvents = function() { + + //copy the instance + var wym = this; + + //handle click event on tools buttons + jQuery(this._box).find(this._options.toolSelector).click(function() { + wym._iframe.contentWindow.focus(); //See #154 + wym.exec(jQuery(this).attr(WYMeditor.NAME)); + return(false); + }); + + //handle click event on containers buttons + jQuery(this._box).find(this._options.containerSelector).click(function() { + wym.container(jQuery(this).attr(WYMeditor.NAME)); + return(false); + }); + + //handle keyup event on html value: set the editor value + //handle focus/blur events to check if the element has focus, see #147 + jQuery(this._box).find(this._options.htmlValSelector) + .keyup(function() { jQuery(wym._doc.body).html(jQuery(this).val());}) + .focus(function() { jQuery(this).toggleClass('hasfocus'); }) + .blur(function() { jQuery(this).toggleClass('hasfocus'); }); + + //handle click event on classes buttons + jQuery(this._box).find(this._options.classSelector).click(function() { + + var aClasses = eval(wym._options.classesItems); + var sName = jQuery(this).attr(WYMeditor.NAME); + + var oClass = WYMeditor.Helper.findByName(aClasses, sName); + + if(oClass) { + var jqexpr = oClass.expr; + wym.toggleClass(sName, jqexpr); + } + wym._iframe.contentWindow.focus(); //See #154 + return(false); + }); + + //handle event on update element + jQuery(this._options.updateSelector) + .bind(this._options.updateEvent, function() { + wym.update(); + }); +}; + +WYMeditor.editor.prototype.ready = function() { + return(this._doc != null); +}; + + +/********** METHODS **********/ + +/* @name box + * @description Returns the WYMeditor container + */ +WYMeditor.editor.prototype.box = function() { + return(this._box); +}; + +/* @name html + * @description Get/Set the html value + */ +WYMeditor.editor.prototype.html = function(html) { + + if(typeof html === 'string') jQuery(this._doc.body).html(html); + else return(jQuery(this._doc.body).html()); +}; + +/* @name xhtml + * @description Cleans up the HTML + */ +WYMeditor.editor.prototype.xhtml = function() { + return this.parser.parse(this.html()); +}; + +/* @name exec + * @description Executes a button command + */ +WYMeditor.editor.prototype.exec = function(cmd) { + + //base function for execCommand + //open a dialog or exec + switch(cmd) { + case WYMeditor.CREATE_LINK: + var container = this.container(); + if(container || this._selected_image) this.dialog(WYMeditor.DIALOG_LINK); + break; + + case WYMeditor.INSERT_IMAGE: + this.dialog(WYMeditor.DIALOG_IMAGE); + break; + + case WYMeditor.INSERT_TABLE: + this.dialog(WYMeditor.DIALOG_TABLE); + break; + + case WYMeditor.PASTE: + this.dialog(WYMeditor.DIALOG_PASTE); + break; + + case WYMeditor.TOGGLE_HTML: + this.update(); + this.toggleHtml(); + + //partially fixes #121 when the user manually inserts an image + if(!jQuery(this._box).find(this._options.htmlSelector).is(':visible')) + this.listen(); + break; + + case WYMeditor.PREVIEW: + this.dialog(WYMeditor.PREVIEW, this._options.dialogFeaturesPreview); + break; + + default: + this._exec(cmd); + break; + } +}; + +/* @name container + * @description Get/Set the selected container + */ +WYMeditor.editor.prototype.container = function(sType) { + + if(sType) { + + var container = null; + + if(sType.toLowerCase() == WYMeditor.TH) { + + container = this.container(); + + //find the TD or TH container + switch(container.tagName.toLowerCase()) { + + case WYMeditor.TD: case WYMeditor.TH: + break; + default: + var aTypes = new Array(WYMeditor.TD,WYMeditor.TH); + container = this.findUp(this.container(), aTypes); + break; + } + + //if it exists, switch + if(container!=null) { + + sType = (container.tagName.toLowerCase() == WYMeditor.TD)? WYMeditor.TH: WYMeditor.TD; + this.switchTo(container,sType); + this.update(); + } + } else { + + //set the container type + var aTypes=new Array(WYMeditor.P,WYMeditor.H1,WYMeditor.H2,WYMeditor.H3,WYMeditor.H4,WYMeditor.H5, + WYMeditor.H6,WYMeditor.PRE,WYMeditor.BLOCKQUOTE); + container = this.findUp(this.container(), aTypes); + + if(container) { + + var newNode = null; + + //blockquotes must contain a block level element + if(sType.toLowerCase() == WYMeditor.BLOCKQUOTE) { + + var blockquote = this.findUp(this.container(), WYMeditor.BLOCKQUOTE); + + if(blockquote == null) { + + newNode = this._doc.createElement(sType); + container.parentNode.insertBefore(newNode,container); + newNode.appendChild(container); + this.setFocusToNode(newNode.firstChild); + + } else { + + var nodes = blockquote.childNodes; + var lgt = nodes.length; + var firstNode = null; + + if(lgt > 0) firstNode = nodes.item(0); + for(var x=0; x<lgt; x++) { + blockquote.parentNode.insertBefore(nodes.item(0),blockquote); + } + blockquote.parentNode.removeChild(blockquote); + if(firstNode) this.setFocusToNode(firstNode); + } + } + + else this.switchTo(container,sType); + + this.update(); + } + } + } + else return(this.selected()); +}; + +/* @name toggleClass + * @description Toggles class on selected element, or one of its parents + */ +WYMeditor.editor.prototype.toggleClass = function(sClass, jqexpr) { + + var container = (this._selected_image + ? this._selected_image + : jQuery(this.selected())); + container = jQuery(container).parentsOrSelf(jqexpr); + jQuery(container).toggleClass(sClass); + + if(!jQuery(container).attr(WYMeditor.CLASS)) jQuery(container).removeAttr(this._class); + +}; + +/* @name findUp + * @description Returns the first parent or self container, based on its type + */ +WYMeditor.editor.prototype.findUp = function(node, filter) { + + //filter is a string or an array of strings + + if(node) { + + var tagname = node.tagName.toLowerCase(); + + if(typeof(filter) == WYMeditor.STRING) { + + while(tagname != filter && tagname != WYMeditor.BODY) { + + node = node.parentNode; + tagname = node.tagName.toLowerCase(); + } + + } else { + + var bFound = false; + + while(!bFound && tagname != WYMeditor.BODY) { + for(var i = 0; i < filter.length; i++) { + if(tagname == filter[i]) { + bFound = true; + break; + } + } + if(!bFound) { + node = node.parentNode; + tagname = node.tagName.toLowerCase(); + } + } + } + + if(tagname != WYMeditor.BODY) return(node); + else return(null); + + } else return(null); +}; + +/* @name switchTo + * @description Switch the node's type + */ +WYMeditor.editor.prototype.switchTo = function(node,sType) { + + var newNode = this._doc.createElement(sType); + var html = jQuery(node).html(); + node.parentNode.replaceChild(newNode,node); + jQuery(newNode).html(html); + this.setFocusToNode(newNode); +}; + +WYMeditor.editor.prototype.replaceStrings = function(sVal) { + //check if the language file has already been loaded + //if not, get it via a synchronous ajax call + if(!WYMeditor.STRINGS[this._options.lang]) { + try { + eval(jQuery.ajax({url:this._options.langPath + + this._options.lang + '.js', async:false}).responseText); + } catch(e) { + WYMeditor.console.error("WYMeditor: error while parsing language file."); + return sVal; + } + } + + //replace all the strings in sVal and return it + for (var key in WYMeditor.STRINGS[this._options.lang]) { + sVal = WYMeditor.Helper.replaceAll(sVal, this._options.stringDelimiterLeft + key + + this._options.stringDelimiterRight, + WYMeditor.STRINGS[this._options.lang][key]); + }; + return(sVal); +}; + +WYMeditor.editor.prototype.encloseString = function(sVal) { + + return(this._options.stringDelimiterLeft + + sVal + + this._options.stringDelimiterRight); +}; + +/* @name status + * @description Prints a status message + */ +WYMeditor.editor.prototype.status = function(sMessage) { + + //print status message + jQuery(this._box).find(this._options.statusSelector).html(sMessage); +}; + +/* @name update + * @description Updates the element and textarea values + */ +WYMeditor.editor.prototype.update = function() { + + var html = this.xhtml(); + jQuery(this._element).val(html); + jQuery(this._box).find(this._options.htmlValSelector).not('.hasfocus').val(html); //#147 +}; + +/* @name dialog + * @description Opens a dialog box + */ +WYMeditor.editor.prototype.dialog = function( dialogType, dialogFeatures, bodyHtml ) { + + var features = dialogFeatures || this._wym._options.dialogFeatures; + var wDialog = window.open('', 'dialog', features); + + if(wDialog) { + + var sBodyHtml = ""; + + switch( dialogType ) { + + case(WYMeditor.DIALOG_LINK): + sBodyHtml = this._options.dialogLinkHtml; + break; + case(WYMeditor.DIALOG_IMAGE): + sBodyHtml = this._options.dialogImageHtml; + break; + case(WYMeditor.DIALOG_TABLE): + sBodyHtml = this._options.dialogTableHtml; + break; + case(WYMeditor.DIALOG_PASTE): + sBodyHtml = this._options.dialogPasteHtml; + break; + case(WYMeditor.PREVIEW): + sBodyHtml = this._options.dialogPreviewHtml; + break; + + default: + sBodyHtml = bodyHtml; + } + + var h = WYMeditor.Helper; + + //construct the dialog + var dialogHtml = this._options.dialogHtml; + dialogHtml = h.replaceAll(dialogHtml, WYMeditor.BASE_PATH, this._options.basePath); + dialogHtml = h.replaceAll(dialogHtml, WYMeditor.DIRECTION, this._options.direction); + dialogHtml = h.replaceAll(dialogHtml, WYMeditor.CSS_PATH, this._options.skinPath + WYMeditor.SKINS_DEFAULT_CSS); + dialogHtml = h.replaceAll(dialogHtml, WYMeditor.WYM_PATH, this._options.wymPath); + dialogHtml = h.replaceAll(dialogHtml, WYMeditor.JQUERY_PATH, this._options.jQueryPath); + dialogHtml = h.replaceAll(dialogHtml, WYMeditor.DIALOG_TITLE, this.encloseString( dialogType )); + dialogHtml = h.replaceAll(dialogHtml, WYMeditor.DIALOG_BODY, sBodyHtml); + dialogHtml = h.replaceAll(dialogHtml, WYMeditor.INDEX, this._index); + + dialogHtml = this.replaceStrings(dialogHtml); + + var doc = wDialog.document; + doc.write(dialogHtml); + doc.close(); + } +}; + +/* @name toggleHtml + * @description Shows/Hides the HTML + */ +WYMeditor.editor.prototype.toggleHtml = function() { + jQuery(this._box).find(this._options.htmlSelector).toggle(); +}; + +WYMeditor.editor.prototype.uniqueStamp = function() { + var now = new Date(); + return("wym-" + now.getTime()); +}; + +WYMeditor.editor.prototype.paste = function(sData) { + + var sTmp; + var container = this.selected(); + + //split the data, using double newlines as the separator + var aP = sData.split(this._newLine + this._newLine); + var rExp = new RegExp(this._newLine, "g"); + + //add a P for each item + if(container && container.tagName.toLowerCase() != WYMeditor.BODY) { + for(x = aP.length - 1; x >= 0; x--) { + sTmp = aP[x]; + //simple newlines are replaced by a break + sTmp = sTmp.replace(rExp, "<br />"); + jQuery(container).after("<p>" + sTmp + "</p>"); + } + } else { + for(x = 0; x < aP.length; x++) { + sTmp = aP[x]; + //simple newlines are replaced by a break + sTmp = sTmp.replace(rExp, "<br />"); + jQuery(this._doc.body).append("<p>" + sTmp + "</p>"); + } + + } +}; + +WYMeditor.editor.prototype.insert = function(html) { + // Do we have a selection? + if (this._iframe.contentWindow.getSelection().focusNode != null) { + // Overwrite selection with provided html + this._exec( WYMeditor.INSERT_HTML, html); + } else { + // Fall back to the internal paste function if there's no selection + this.paste(html) + } +}; + +WYMeditor.editor.prototype.wrap = function(left, right) { + // Do we have a selection? + if (this._iframe.contentWindow.getSelection().focusNode != null) { + // Wrap selection with provided html + this._exec( WYMeditor.INSERT_HTML, left + this._iframe.contentWindow.getSelection().toString() + right); + } +}; + +WYMeditor.editor.prototype.unwrap = function() { + // Do we have a selection? + if (this._iframe.contentWindow.getSelection().focusNode != null) { + // Unwrap selection + this._exec( WYMeditor.INSERT_HTML, this._iframe.contentWindow.getSelection().toString() ); + } +}; + +WYMeditor.editor.prototype.addCssRules = function(doc, aCss) { + var styles = doc.styleSheets[0]; + if(styles) { + for(var i = 0; i < aCss.length; i++) { + var oCss = aCss[i]; + if(oCss.name && oCss.css) this.addCssRule(styles, oCss); + } + } +}; + +/********** CONFIGURATION **********/ + +WYMeditor.editor.prototype.computeBasePath = function() { + return jQuery(jQuery.grep(jQuery('script'), function(s){ + return (s.src && s.src.match(/jquery\.wymeditor(\.pack|\.min|\.packed)?\.js(\?.*)?$/ )) + })).attr('src').replace(/jquery\.wymeditor(\.pack|\.min|\.packed)?\.js(\?.*)?$/, ''); +}; + +WYMeditor.editor.prototype.computeWymPath = function() { + return jQuery(jQuery.grep(jQuery('script'), function(s){ + return (s.src && s.src.match(/jquery\.wymeditor(\.pack|\.min|\.packed)?\.js(\?.*)?$/ )) + })).attr('src'); +}; + +WYMeditor.editor.prototype.computeJqueryPath = function() { + return jQuery(jQuery.grep(jQuery('script'), function(s){ + return (s.src && s.src.match(/jquery(-(.*)){0,1}(\.pack|\.min|\.packed)?\.js(\?.*)?$/ )) + })).attr('src'); +}; + +WYMeditor.editor.prototype.computeCssPath = function() { + return jQuery(jQuery.grep(jQuery('link'), function(s){ + return (s.href && s.href.match(/wymeditor\/skins\/(.*)screen\.css(\?.*)?$/ )) + })).attr('href'); +}; + +WYMeditor.editor.prototype.configureEditorUsingRawCss = function() { + + var CssParser = new WYMeditor.WymCssParser(); + if(this._options.stylesheet){ + CssParser.parse(jQuery.ajax({url: this._options.stylesheet,async:false}).responseText); + }else{ + CssParser.parse(this._options.styles, false); + } + + if(this._options.classesItems.length == 0) { + this._options.classesItems = CssParser.css_settings.classesItems; + } + if(this._options.editorStyles.length == 0) { + this._options.editorStyles = CssParser.css_settings.editorStyles; + } + if(this._options.dialogStyles.length == 0) { + this._options.dialogStyles = CssParser.css_settings.dialogStyles; + } +}; + +/********** EVENTS **********/ + +WYMeditor.editor.prototype.listen = function() { + + //don't use jQuery.find() on the iframe body + //because of MSIE + jQuery + expando issue (#JQ1143) + //jQuery(this._doc.body).find("*").bind("mouseup", this.mouseup); + + jQuery(this._doc.body).bind("mousedown", this.mousedown); + var images = this._doc.body.getElementsByTagName("img"); + for(var i=0; i < images.length; i++) { + jQuery(images[i]).bind("mousedown", this.mousedown); + } +}; + +WYMeditor.editor.prototype.mousedown = function(evt) { + + var wym = WYMeditor.INSTANCES[this.ownerDocument.title]; + wym._selected_image = (this.tagName.toLowerCase() == WYMeditor.IMG) ? this : null; + evt.stopPropagation(); +}; + +/********** SKINS **********/ + +/* + * Function: WYMeditor.loadCss + * Loads a stylesheet in the document. + * + * Parameters: + * href - The CSS path. + */ +WYMeditor.loadCss = function(href) { + + var link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = href; + + var head = jQuery('head').get(0); + head.appendChild(link); +}; + +/* + * Function: WYMeditor.editor.loadSkin + * Loads the skin CSS and initialization script (if needed). + */ +WYMeditor.editor.prototype.loadSkin = function() { + + //does the user want to automatically load the CSS (default: yes)? + //we also test if it hasn't been already loaded by another instance + //see below for a better (second) test + if(this._options.loadSkin && !WYMeditor.SKINS[this._options.skin]) { + + //check if it hasn't been already loaded + //so we don't load it more than once + //(we check the existing <link> elements) + + var found = false; + var rExp = new RegExp(this._options.skin + + '\/' + WYMeditor.SKINS_DEFAULT_CSS + '$'); + + jQuery('link').each( function() { + if(this.href.match(rExp)) found = true; + }); + + //load it, using the skin path + if(!found) WYMeditor.loadCss( this._options.skinPath + + WYMeditor.SKINS_DEFAULT_CSS ); + } + + //put the classname (ex. wym_skin_default) on wym_box + jQuery(this._box).addClass( "wym_skin_" + this._options.skin ); + + //does the user want to use some JS to initialize the skin (default: yes)? + //also check if it hasn't already been loaded by another instance + if(this._options.initSkin && !WYMeditor.SKINS[this._options.skin]) { + + eval(jQuery.ajax({url:this._options.skinPath + + WYMeditor.SKINS_DEFAULT_JS, async:false}).responseText); + } + + //init the skin, if needed + if(WYMeditor.SKINS[this._options.skin] + && WYMeditor.SKINS[this._options.skin].init) + WYMeditor.SKINS[this._options.skin].init(this); + +}; + + +/********** DIALOGS **********/ + +WYMeditor.INIT_DIALOG = function(index) { + + var wym = window.opener.WYMeditor.INSTANCES[index]; + var doc = window.document; + var selected = wym.selected(); + var dialogType = jQuery(wym._options.dialogTypeSelector).val(); + var sStamp = wym.uniqueStamp(); + + switch(dialogType) { + + case WYMeditor.DIALOG_LINK: + //ensure that we select the link to populate the fields + if(selected && selected.tagName && selected.tagName.toLowerCase != WYMeditor.A) + selected = jQuery(selected).parentsOrSelf(WYMeditor.A); + + //fix MSIE selection if link image has been clicked + if(!selected && wym._selected_image) + selected = jQuery(wym._selected_image).parentsOrSelf(WYMeditor.A); + break; + + } + + //pre-init functions + if(jQuery.isFunction(wym._options.preInitDialog)) + wym._options.preInitDialog(wym,window); + + //add css rules from options + var styles = doc.styleSheets[0]; + var aCss = eval(wym._options.dialogStyles); + + wym.addCssRules(doc, aCss); + + //auto populate fields if selected container (e.g. A) + if(selected) { + jQuery(wym._options.hrefSelector).val(jQuery(selected).attr(WYMeditor.HREF)); + jQuery(wym._options.srcSelector).val(jQuery(selected).attr(WYMeditor.SRC)); + jQuery(wym._options.titleSelector).val(jQuery(selected).attr(WYMeditor.TITLE)); + jQuery(wym._options.altSelector).val(jQuery(selected).attr(WYMeditor.ALT)); + } + + //auto populate image fields if selected image + if(wym._selected_image) { + jQuery(wym._options.dialogImageSelector + " " + wym._options.srcSelector) + .val(jQuery(wym._selected_image).attr(WYMeditor.SRC)); + jQuery(wym._options.dialogImageSelector + " " + wym._options.titleSelector) + .val(jQuery(wym._selected_image).attr(WYMeditor.TITLE)); + jQuery(wym._options.dialogImageSelector + " " + wym._options.altSelector) + .val(jQuery(wym._selected_image).attr(WYMeditor.ALT)); + } + + jQuery(wym._options.dialogLinkSelector + " " + + wym._options.submitSelector).click(function() { + + var sUrl = jQuery(wym._options.hrefSelector).val(); + if(sUrl.length > 0) { + + wym._exec(WYMeditor.CREATE_LINK, sStamp); + + jQuery("a[href=" + sStamp + "]", wym._doc.body) + .attr(WYMeditor.HREF, sUrl) + .attr(WYMeditor.TITLE, jQuery(wym._options.titleSelector).val()); + + } + window.close(); + }); + + jQuery(wym._options.dialogImageSelector + " " + + wym._options.submitSelector).click(function() { + + var sUrl = jQuery(wym._options.srcSelector).val(); + if(sUrl.length > 0) { + + wym._exec(WYMeditor.INSERT_IMAGE, sStamp); + + jQuery("img[src$=" + sStamp + "]", wym._doc.body) + .attr(WYMeditor.SRC, sUrl) + .attr(WYMeditor.TITLE, jQuery(wym._options.titleSelector).val()) + .attr(WYMeditor.ALT, jQuery(wym._options.altSelector).val()); + } + window.close(); + }); + + jQuery(wym._options.dialogTableSelector + " " + + wym._options.submitSelector).click(function() { + + var iRows = jQuery(wym._options.rowsSelector).val(); + var iCols = jQuery(wym._options.colsSelector).val(); + + if(iRows > 0 && iCols > 0) { + + var table = wym._doc.createElement(WYMeditor.TABLE); + var newRow = null; + var newCol = null; + + var sCaption = jQuery(wym._options.captionSelector).val(); + + //we create the caption + var newCaption = table.createCaption(); + newCaption.innerHTML = sCaption; + + //we create the rows and cells + for(x=0; x<iRows; x++) { + newRow = table.insertRow(x); + for(y=0; y<iCols; y++) {newRow.insertCell(y);} + } + + //set the summary attr + jQuery(table).attr('summary', + jQuery(wym._options.summarySelector).val()); + + //append the table after the selected container + var node = jQuery(wym.findUp(wym.container(), + WYMeditor.MAIN_CONTAINERS)).get(0); + if(!node || !node.parentNode) jQuery(wym._doc.body).append(table); + else jQuery(node).after(table); + } + window.close(); + }); + + jQuery(wym._options.dialogPasteSelector + " " + + wym._options.submitSelector).click(function() { + + var sText = jQuery(wym._options.textSelector).val(); + wym.paste(sText); + window.close(); + }); + + jQuery(wym._options.dialogPreviewSelector + " " + + wym._options.previewSelector) + .html(wym.xhtml()); + + //cancel button + jQuery(wym._options.cancelSelector).mousedown(function() { + window.close(); + }); + + //pre-init functions + if(jQuery.isFunction(wym._options.postInitDialog)) + wym._options.postInitDialog(wym,window); + +}; + +/********** XHTML LEXER/PARSER **********/ + +/* +* @name xml +* @description Use these methods to generate XML and XHTML compliant tags and +* escape tag attributes correctly +* @author Bermi Ferrer - http://bermi.org +* @author David Heinemeier Hansson http://loudthinking.com +*/ +WYMeditor.XmlHelper = function() +{ + this._entitiesDiv = document.createElement('div'); + return this; +}; + + +/* +* @name tag +* @description +* Returns an empty HTML tag of type *name* which by default is XHTML +* compliant. Setting *open* to true will create an open tag compatible +* with HTML 4.0 and below. Add HTML attributes by passing an attributes +* array to *options*. For attributes with no value like (disabled and +* readonly), give it a value of true in the *options* array. +* +* Examples: +* +* this.tag('br') +* # => <br /> +* this.tag ('br', false, true) +* # => <br> +* this.tag ('input', jQuery({type:'text',disabled:true }) ) +* # => <input type="text" disabled="disabled" /> +*/ +WYMeditor.XmlHelper.prototype.tag = function(name, options, open) +{ + options = options || false; + open = open || false; + return '<'+name+(options ? this.tagOptions(options) : '')+(open ? '>' : ' />'); +}; + +/* +* @name contentTag +* @description +* Returns a XML block tag of type *name* surrounding the *content*. Add +* XML attributes by passing an attributes array to *options*. For attributes +* with no value like (disabled and readonly), give it a value of true in +* the *options* array. You can use symbols or strings for the attribute names. +* +* this.contentTag ('p', 'Hello world!' ) +* # => <p>Hello world!</p> +* this.contentTag('div', this.contentTag('p', "Hello world!"), jQuery({class : "strong"})) +* # => <div class="strong"><p>Hello world!</p></div> +* this.contentTag("select", options, jQuery({multiple : true})) +* # => <select multiple="multiple">...options...</select> +*/ +WYMeditor.XmlHelper.prototype.contentTag = function(name, content, options) +{ + options = options || false; + return '<'+name+(options ? this.tagOptions(options) : '')+'>'+content+'</'+name+'>'; +}; + +/* +* @name cdataSection +* @description +* Returns a CDATA section for the given +content+. CDATA sections +* are used to escape blocks of text containing characters which would +* otherwise be recognized as markup. CDATA sections begin with the string +* <tt><![CDATA[</tt> and } with (and may not contain) the string +* <tt>]]></tt>. +*/ +WYMeditor.XmlHelper.prototype.cdataSection = function(content) +{ + return '<![CDATA['+content+']]>'; +}; + + +/* +* @name escapeOnce +* @description +* Returns the escaped +xml+ without affecting existing escaped entities. +* +* this.escapeOnce( "1 > 2 & 3") +* # => "1 > 2 & 3" +*/ +WYMeditor.XmlHelper.prototype.escapeOnce = function(xml) +{ + return this._fixDoubleEscape(this.escapeEntities(xml)); +}; + +/* +* @name _fixDoubleEscape +* @description +* Fix double-escaped entities, such as &, {, etc. +*/ +WYMeditor.XmlHelper.prototype._fixDoubleEscape = function(escaped) +{ + return escaped.replace(/&([a-z]+|(#\d+));/ig, "&$1;"); +}; + +/* +* @name tagOptions +* @description +* Takes an array like the one generated by Tag.parseAttributes +* [["src", "http://www.editam.com/?a=b&c=d&f=g"], ["title", "Editam, <Simplified> CMS"]] +* or an object like {src:"http://www.editam.com/?a=b&c=d&f=g", title:"Editam, <Simplified> CMS"} +* and returns a string properly escaped like +* ' src = "http://www.editam.com/?a=b&c=d&f=g" title = "Editam, <Simplified> CMS"' +* which is valid for strict XHTML +*/ +WYMeditor.XmlHelper.prototype.tagOptions = function(options) +{ + var xml = this; + xml._formated_options = ''; + + for (var key in options) { + var formated_options = ''; + var value = options[key]; + if(typeof value != 'function' && value.length > 0) { + + if(parseInt(key) == key && typeof value == 'object'){ + key = value.shift(); + value = value.pop(); + } + if(key != '' && value != ''){ + xml._formated_options += ' '+key+'="'+xml.escapeOnce(value)+'"'; + } + } + } + return xml._formated_options; +}; + +/* +* @name escapeEntities +* @description +* Escapes XML/HTML entities <, >, & and ". If seccond parameter is set to false it +* will not escape ". If set to true it will also escape ' +*/ +WYMeditor.XmlHelper.prototype.escapeEntities = function(string, escape_quotes) +{ + this._entitiesDiv.innerHTML = string; + this._entitiesDiv.textContent = string; + var result = this._entitiesDiv.innerHTML; + if(typeof escape_quotes == 'undefined'){ + if(escape_quotes != false) result = result.replace('"', '"'); + if(escape_quotes == true) result = result.replace('"', '''); + } + return result; +}; + +/* +* Parses a string conatining tag attributes and values an returns an array formated like +* [["src", "http://www.editam.com"], ["title", "Editam, Simplified CMS"]] +*/ +WYMeditor.XmlHelper.prototype.parseAttributes = function(tag_attributes) +{ + // Use a compounded regex to match single quoted, double quoted and unquoted attribute pairs + var result = []; + var matches = tag_attributes.split(/((=\s*")(")("))|((=\s*\')(\')(\'))|((=\s*[^>\s]*))/g); + if(matches.toString() != tag_attributes){ + for (var k in matches) { + var v = matches[k]; + if(typeof v != 'function' && v.length != 0){ + var re = new RegExp('(\\w+)\\s*'+v); + if(match = tag_attributes.match(re) ){ + var value = v.replace(/^[\s=]+/, ""); + var delimiter = value.charAt(0); + delimiter = delimiter == '"' ? '"' : (delimiter=="'"?"'":''); + if(delimiter != ''){ + value = delimiter == '"' ? value.replace(/^"|"+$/g, '') : value.replace(/^'|'+$/g, ''); + } + tag_attributes = tag_attributes.replace(match[0],''); + result.push([match[1] , value]); + } + } + } + } + return result; +}; + +/** +* XhtmlValidator for validating tag attributes +* +* @author Bermi Ferrer - http://bermi.org +*/ +WYMeditor.XhtmlValidator = { + "_attributes": + { + "core": + { + "except":[ + "base", + "head", + "html", + "meta", + "param", + "script", + "style", + "title" + ], + "attributes":[ + "class", + "id", + "style", + "title", + "accesskey", + "tabindex" + ] + }, + "language": + { + "except":[ + "base", + "br", + "hr", + "iframe", + "param", + "script" + ], + "attributes": + { + "dir":[ + "ltr", + "rtl" + ], + "0":"lang", + "1":"xml:lang" + } + }, + "keyboard": + { + "attributes": + { + "accesskey":/^(\w){1}$/, + "tabindex":/^(\d)+$/ + } + } + }, + "_events": + { + "window": + { + "only":[ + "body" + ], + "attributes":[ + "onload", + "onunload" + ] + }, + "form": + { + "only":[ + "form", + "input", + "textarea", + "select", + "a", + "label", + "button" + ], + "attributes":[ + "onchange", + "onsubmit", + "onreset", + "onselect", + "onblur", + "onfocus" + ] + }, + "keyboard": + { + "except":[ + "base", + "bdo", + "br", + "frame", + "frameset", + "head", + "html", + "iframe", + "meta", + "param", + "script", + "style", + "title" + ], + "attributes":[ + "onkeydown", + "onkeypress", + "onkeyup" + ] + }, + "mouse": + { + "except":[ + "base", + "bdo", + "br", + "head", + "html", + "meta", + "param", + "script", + "style", + "title" + ], + "attributes":[ + "onclick", + "ondblclick", + "onmousedown", + "onmousemove", + "onmouseover", + "onmouseout", + "onmouseup" + ] + } + }, + "_tags": + { + "a": + { + "attributes": + { + "0":"charset", + "1":"coords", + "2":"href", + "3":"hreflang", + "4":"name", + "rel":/^(alternate|designates|stylesheet|start|next|prev|contents|index|glossary|copyright|chapter|section|subsection|appendix|help|bookmark| |shortcut|icon)+$/, + "rev":/^(alternate|designates|stylesheet|start|next|prev|contents|index|glossary|copyright|chapter|section|subsection|appendix|help|bookmark| |shortcut|icon)+$/, + "shape":/^(rect|rectangle|circ|circle|poly|polygon)$/, + "5":"type" + } + }, + "0":"abbr", + "1":"acronym", + "2":"address", + "area": + { + "attributes": + { + "0":"alt", + "1":"coords", + "2":"href", + "nohref":/^(true|false)$/, + "shape":/^(rect|rectangle|circ|circle|poly|polygon)$/ + }, + "required":[ + "alt" + ] + }, + "3":"b", + "base": + { + "attributes":[ + "href" + ], + "required":[ + "href" + ] + }, + "bdo": + { + "attributes": + { + "dir":/^(ltr|rtl)$/ + }, + "required":[ + "dir" + ] + }, + "4":"big", + "blockquote": + { + "attributes":[ + "cite" + ] + }, + "5":"body", + "6":"br", + "button": + { + "attributes": + { + "disabled":/^(disabled)$/, + "type":/^(button|reset|submit)$/, + "0":"value" + }, + "inside":"form" + }, + "7":"caption", + "8":"cite", + "9":"code", + "col": + { + "attributes": + { + "align":/^(right|left|center|justify)$/, + "0":"char", + "1":"charoff", + "span":/^(\d)+$/, + "valign":/^(top|middle|bottom|baseline)$/, + "2":"width" + }, + "inside":"colgroup" + }, + "colgroup": + { + "attributes": + { + "align":/^(right|left|center|justify)$/, + "0":"char", + "1":"charoff", + "span":/^(\d)+$/, + "valign":/^(top|middle|bottom|baseline)$/, + "2":"width" + } + }, + "10":"dd", + "del": + { + "attributes": + { + "0":"cite", + "datetime":/^([0-9]){8}/ + } + }, + "11":"div", + "12":"dfn", + "13":"dl", + "14":"dt", + "15":"em", + "fieldset": + { + "inside":"form" + }, + "form": + { + "attributes": + { + "0":"action", + "1":"accept", + "2":"accept-charset", + "3":"enctype", + "method":/^(get|post)$/ + }, + "required":[ + "action" + ] + }, + "head": + { + "attributes":[ + "profile" + ] + }, + "16":"h1", + "17":"h2", + "18":"h3", + "19":"h4", + "20":"h5", + "21":"h6", + "22":"hr", + "html": + { + "attributes":[ + "xmlns" + ] + }, + "23":"i", + "img": + { + "attributes":[ + "alt", + "src", + "height", + "ismap", + "longdesc", + "usemap", + "width" + ], + "required":[ + "alt", + "src" + ] + }, + "input": + { + "attributes": + { + "0":"accept", + "1":"alt", + "checked":/^(checked)$/, + "disabled":/^(disabled)$/, + "maxlength":/^(\d)+$/, + "2":"name", + "readonly":/^(readonly)$/, + "size":/^(\d)+$/, + "3":"src", + "type":/^(button|checkbox|file|hidden|image|password|radio|reset|submit|text)$/, + "4":"value" + }, + "inside":"form" + }, + "ins": + { + "attributes": + { + "0":"cite", + "datetime":/^([0-9]){8}/ + } + }, + "24":"kbd", + "label": + { + "attributes":[ + "for" + ], + "inside":"form" + }, + "25":"legend", + "26":"li", + "link": + { + "attributes": + { + "0":"charset", + "1":"href", + "2":"hreflang", + "media":/^(all|braille|print|projection|screen|speech|,|;| )+$/i, + //next comment line required by Opera! + /*"rel":/^(alternate|appendix|bookmark|chapter|contents|copyright|glossary|help|home|index|next|prev|section|start|stylesheet|subsection| |shortcut|icon)+$/i,*/ + "rel":/^(alternate|appendix|bookmark|chapter|contents|copyright|glossary|help|home|index|next|prev|section|start|stylesheet|subsection| |shortcut|icon)+$/i, + "rev":/^(alternate|appendix|bookmark|chapter|contents|copyright|glossary|help|home|index|next|prev|section|start|stylesheet|subsection| |shortcut|icon)+$/i, + "3":"type" + }, + "inside":"head" + }, + "map": + { + "attributes":[ + "id", + "name"