diff --git a/wiki/parser/Box.py b/wiki/parser/Box.py new file mode 100644 index 00000000..e269d318 --- /dev/null +++ b/wiki/parser/Box.py @@ -0,0 +1,110 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - Portail parser + + PURPOSE: + une boite jolie + + CALLING SEQUENCE: + {{{ + #!Box titre + blablabla + + tables, images.... + }}} + +""" +from MoinMoin.parser import wiki +import os,string,re,StringIO +from MoinMoin.action import AttachFile + +##################################################################### +# Fonctions # +##################################################################### +# to_wikiname : transfome la chaine text avec le parser wiki +# classique et le formateur formatter +# (je sais pas a quoi sert request, mais faut pas +# l'enlever !) +####### + +def to_wikiname(request,formatter,text): + ##taken from MiniPage + out=StringIO.StringIO() + request.redirect(out) + wikiizer = wiki.Parser(text,request) + wikiizer.format(formatter) + result=out.getvalue() + request.redirect() + del out + + return result.strip() + +##################################################################### +# BoxFormatter : creer le code html +####### +class BoxFormatter: + + def __init__(self, request, formatter): + self.formatter = formatter + self.request = request + self.counter = 0 + + def make(self, title, body_html, color=None): + if color==None: + css_color_class=u"" + else: + css_color_class = u" %s_box" % color + html = [ + self.formatter.rawHTML(u'
' % css_color_class), + self.formatter.rawHTML(u'
'), + self.formatter.heading(1,3), + title, + self.formatter.heading(0,3), + self.formatter.rawHTML(u'
'), + self.formatter.rawHTML(u'
'), + self.formatter.rawHTML(u'
'), + body_html, + self.formatter.rawHTML(u'
'), + self.formatter.rawHTML(u'
'), + self.formatter.rawHTML(u'
'), + ] + self.request.write(u'\n'.join(html)) + +##################################################################### +# Parser : classe principale, c'est elle qui parse +####### +class Parser: + + def __init__(self, raw, request, **kw): + self.request = request + self.raw = raw + self.settings={'title': u'|'.join([u" ".join(kw.keys()), u" ".join(kw.values())])} + self.settings = self.parseArgs(kw["format_args"]) + + + def parseArgs(self, argsString): + argList = argsString.split(u',') + settings = {} + for anArg in argList: + anArg = anArg.strip(u' ') + if anArg.find(u'color=')!=-1: + settings['color'] = anArg.split(u'=')[1] + else: + settings['title'] = anArg + return settings + + def format(self, formatter): + quotes = self.raw + + # on utilise la classe qui va fabriquer le code html + boite = BoxFormatter(self.request, formatter) + title = self.settings['title'] + content = to_wikiname(self.request, formatter, quotes) + try: + color = self.settings['color'] + except: + color=None + boite.make( title,content, color) + return + + diff --git a/wiki/parser/EXIF.py b/wiki/parser/EXIF.py new file mode 100644 index 00000000..ab9dee0f --- /dev/null +++ b/wiki/parser/EXIF.py @@ -0,0 +1,1193 @@ +# Library to extract EXIF information in digital camera image files +# +# To use this library call with: +# f=open(path_name, 'rb') +# tags=EXIF.process_file(f) +# tags will now be a dictionary mapping names of EXIF tags to their +# values in the file named by path_name. You can process the tags +# as you wish. In particular, you can iterate through all the tags with: +# for tag in tags.keys(): +# if tag not in ('JPEGThumbnail', 'TIFFThumbnail', 'Filename', +# 'EXIF MakerNote'): +# print "Key: %s, value %s" % (tag, tags[tag]) +# (This code uses the if statement to avoid printing out a few of the +# tags that tend to be long or boring.) +# +# The tags dictionary will include keys for all of the usual EXIF +# tags, and will also include keys for Makernotes used by some +# cameras, for which we have a good specification. +# +# Contains code from "exifdump.py" originally written by Thierry Bousch +# and released into the public domain. +# +# Updated and turned into general-purpose library by Gene Cash +# +# This copyright license is intended to be similar to the FreeBSD license. +# +# Copyright 2002 Gene Cash All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY GENE CASH ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# This means you may do anything you want with this code, except claim you +# wrote it. Also, if it breaks you get to keep both pieces. +# +# Patch Contributors: +# * Simon J. Gerraty +# s2n fix & orientation decode +# * John T. Riedl +# Added support for newer Nikon type 3 Makernote format for D70 and some +# other Nikon cameras. +# * Joerg Schaefer +# Fixed subtle bug when faking an EXIF header, which affected maker notes +# using relative offsets, and a fix for Nikon D100. +# +# 21-AUG-99 TB Last update by Thierry Bousch to his code. +# 17-JAN-02 CEC Discovered code on web. +# Commented everything. +# Made small code improvements. +# Reformatted for readability. +# 19-JAN-02 CEC Added ability to read TIFFs and JFIF-format JPEGs. +# Added ability to extract JPEG formatted thumbnail. +# Added ability to read GPS IFD (not tested). +# Converted IFD data structure to dictionaries indexed by +# tag name. +# Factored into library returning dictionary of IFDs plus +# thumbnail, if any. +# 20-JAN-02 CEC Added MakerNote processing logic. +# Added Olympus MakerNote. +# Converted data structure to single-level dictionary, avoiding +# tag name collisions by prefixing with IFD name. This makes +# it much easier to use. +# 23-JAN-02 CEC Trimmed nulls from end of string values. +# 25-JAN-02 CEC Discovered JPEG thumbnail in Olympus TIFF MakerNote. +# 26-JAN-02 CEC Added ability to extract TIFF thumbnails. +# Added Nikon, Fujifilm, Casio MakerNotes. +# 30-NOV-03 CEC Fixed problem with canon_decode_tag() not creating an +# IFD_Tag() object. +# 15-FEB-04 CEC Finally fixed bit shift warning by converting Y to 0L. +# + +# field type descriptions as (length, abbreviation, full name) tuples +FIELD_TYPES=( + (0, 'X', 'Proprietary'), # no such type + (1, 'B', 'Byte'), + (1, 'A', 'ASCII'), + (2, 'S', 'Short'), + (4, 'L', 'Long'), + (8, 'R', 'Ratio'), + (1, 'SB', 'Signed Byte'), + (1, 'U', 'Undefined'), + (2, 'SS', 'Signed Short'), + (4, 'SL', 'Signed Long'), + (8, 'SR', 'Signed Ratio') + ) + +# dictionary of main EXIF tag names +# first element of tuple is tag name, optional second element is +# another dictionary giving names to values +EXIF_TAGS={ + 0x0100: ('ImageWidth', ), + 0x0101: ('ImageLength', ), + 0x0102: ('BitsPerSample', ), + 0x0103: ('Compression', + {1: 'Uncompressed TIFF', + 6: 'JPEG Compressed'}), + 0x0106: ('PhotometricInterpretation', ), + 0x010A: ('FillOrder', ), + 0x010D: ('DocumentName', ), + 0x010E: ('ImageDescription', ), + 0x010F: ('Make', ), + 0x0110: ('Model', ), + 0x0111: ('StripOffsets', ), + 0x0112: ('Orientation', + {1: 'Horizontal (normal)', + 2: 'Mirrored horizontal', + 3: 'Rotated 180', + 4: 'Mirrored vertical', + 5: 'Mirrored horizontal then rotated 90 CCW', + 6: 'Rotated 90 CW', + 7: 'Mirrored horizontal then rotated 90 CW', + 8: 'Rotated 90 CCW'}), + 0x0115: ('SamplesPerPixel', ), + 0x0116: ('RowsPerStrip', ), + 0x0117: ('StripByteCounts', ), + 0x011A: ('XResolution', ), + 0x011B: ('YResolution', ), + 0x011C: ('PlanarConfiguration', ), + 0x0128: ('ResolutionUnit', + {1: 'Not Absolute', + 2: 'Pixels/Inch', + 3: 'Pixels/Centimeter'}), + 0x012D: ('TransferFunction', ), + 0x0131: ('Software', ), + 0x0132: ('DateTime', ), + 0x013B: ('Artist', ), + 0x013E: ('WhitePoint', ), + 0x013F: ('PrimaryChromaticities', ), + 0x0156: ('TransferRange', ), + 0x0200: ('JPEGProc', ), + 0x0201: ('JPEGInterchangeFormat', ), + 0x0202: ('JPEGInterchangeFormatLength', ), + 0x0211: ('YCbCrCoefficients', ), + 0x0212: ('YCbCrSubSampling', ), + 0x0213: ('YCbCrPositioning', ), + 0x0214: ('ReferenceBlackWhite', ), + 0x828D: ('CFARepeatPatternDim', ), + 0x828E: ('CFAPattern', ), + 0x828F: ('BatteryLevel', ), + 0x8298: ('Copyright', ), + 0x829A: ('ExposureTime', ), + 0x829D: ('FNumber', ), + 0x83BB: ('IPTC/NAA', ), + 0x8769: ('ExifOffset', ), + 0x8773: ('InterColorProfile', ), + 0x8822: ('ExposureProgram', + {0: 'Unidentified', + 1: 'Manual', + 2: 'Program Normal', + 3: 'Aperture Priority', + 4: 'Shutter Priority', + 5: 'Program Creative', + 6: 'Program Action', + 7: 'Portrait Mode', + 8: 'Landscape Mode'}), + 0x8824: ('SpectralSensitivity', ), + 0x8825: ('GPSInfo', ), + 0x8827: ('ISOSpeedRatings', ), + 0x8828: ('OECF', ), + # print as string + #0x9000: ('ExifVersion', lambda x: ''.join(map(chr, x))), + 0x9003: ('DateTimeOriginal', ), + 0x9004: ('DateTimeDigitized', ), + 0x9101: ('ComponentsConfiguration', + {0: '', + 1: 'Y', + 2: 'Cb', + 3: 'Cr', + 4: 'Red', + 5: 'Green', + 6: 'Blue'}), + 0x9102: ('CompressedBitsPerPixel', ), + 0x9201: ('ShutterSpeedValue', ), + 0x9202: ('ApertureValue', ), + 0x9203: ('BrightnessValue', ), + 0x9204: ('ExposureBiasValue', ), + 0x9205: ('MaxApertureValue', ), + 0x9206: ('SubjectDistance', ), + 0x9207: ('MeteringMode', + {0: 'Unidentified', + 1: 'Average', + 2: 'CenterWeightedAverage', + 3: 'Spot', + 4: 'MultiSpot'}), + 0x9208: ('LightSource', + {0: 'Unknown', + 1: 'Daylight', + 2: 'Fluorescent', + 3: 'Tungsten', + 10: 'Flash', + 17: 'Standard Light A', + 18: 'Standard Light B', + 19: 'Standard Light C', + 20: 'D55', + 21: 'D65', + 22: 'D75', + 255: 'Other'}), + 0x9209: ('Flash', {0: 'No', + 1: 'Fired', + 5: 'Fired (?)', # no return sensed + 7: 'Fired (!)', # return sensed + 9: 'Fill Fired', + 13: 'Fill Fired (?)', + 15: 'Fill Fired (!)', + 16: 'Off', + 24: 'Auto Off', + 25: 'Auto Fired', + 29: 'Auto Fired (?)', + 31: 'Auto Fired (!)', + 32: 'Not Available'}), + 0x920A: ('FocalLength', ), + 0x927C: ('MakerNote', ), + # print as string + #0x9286: ('UserComment', lambda x: ''.join(map(chr, x))), + 0x9290: ('SubSecTime', ), + 0x9291: ('SubSecTimeOriginal', ), + 0x9292: ('SubSecTimeDigitized', ), + # print as string + #0xA000: ('FlashPixVersion', lambda x: ''.join(map(chr, x))), + 0xA001: ('ColorSpace', ), + 0xA002: ('ExifImageWidth', ), + 0xA003: ('ExifImageLength', ), + 0xA005: ('InteroperabilityOffset', ), + 0xA20B: ('FlashEnergy', ), # 0x920B in TIFF/EP + 0xA20C: ('SpatialFrequencyResponse', ), # 0x920C - - + 0xA20E: ('FocalPlaneXResolution', ), # 0x920E - - + 0xA20F: ('FocalPlaneYResolution', ), # 0x920F - - + 0xA210: ('FocalPlaneResolutionUnit', ), # 0x9210 - - + 0xA214: ('SubjectLocation', ), # 0x9214 - - + 0xA215: ('ExposureIndex', ), # 0x9215 - - + 0xA217: ('SensingMethod', ), # 0x9217 - - + 0xA300: ('FileSource', + {3: 'Digital Camera'}), + 0xA301: ('SceneType', + {1: 'Directly Photographed'}), + 0xA302: ('CVAPattern',), + } + +# interoperability tags +INTR_TAGS={ + 0x0001: ('InteroperabilityIndex', ), + 0x0002: ('InteroperabilityVersion', ), + 0x1000: ('RelatedImageFileFormat', ), + 0x1001: ('RelatedImageWidth', ), + 0x1002: ('RelatedImageLength', ), + } + +# GPS tags (not used yet, haven't seen camera with GPS) +GPS_TAGS={ + 0x0000: ('GPSVersionID', ), + 0x0001: ('GPSLatitudeRef', ), + 0x0002: ('GPSLatitude', ), + 0x0003: ('GPSLongitudeRef', ), + 0x0004: ('GPSLongitude', ), + 0x0005: ('GPSAltitudeRef', ), + 0x0006: ('GPSAltitude', ), + 0x0007: ('GPSTimeStamp', ), + 0x0008: ('GPSSatellites', ), + 0x0009: ('GPSStatus', ), + 0x000A: ('GPSMeasureMode', ), + 0x000B: ('GPSDOP', ), + 0x000C: ('GPSSpeedRef', ), + 0x000D: ('GPSSpeed', ), + 0x000E: ('GPSTrackRef', ), + 0x000F: ('GPSTrack', ), + 0x0010: ('GPSImgDirectionRef', ), + 0x0011: ('GPSImgDirection', ), + 0x0012: ('GPSMapDatum', ), + 0x0013: ('GPSDestLatitudeRef', ), + 0x0014: ('GPSDestLatitude', ), + 0x0015: ('GPSDestLongitudeRef', ), + 0x0016: ('GPSDestLongitude', ), + 0x0017: ('GPSDestBearingRef', ), + 0x0018: ('GPSDestBearing', ), + 0x0019: ('GPSDestDistanceRef', ), + 0x001A: ('GPSDestDistance', ) + } + +# Nikon E99x MakerNote Tags +# http://members.tripod.com/~tawba/990exif.htm +MAKERNOTE_NIKON_NEWER_TAGS={ + 0x0002: ('ISOSetting', ), + 0x0003: ('ColorMode', ), + 0x0004: ('Quality', ), + 0x0005: ('Whitebalance', ), + 0x0006: ('ImageSharpening', ), + 0x0007: ('FocusMode', ), + 0x0008: ('FlashSetting', ), + 0x0009: ('AutoFlashMode', ), + 0x000B: ('WhiteBalanceBias', ), + 0x000C: ('WhiteBalanceRBCoeff', ), + 0x000F: ('ISOSelection', ), + 0x0012: ('FlashCompensation', ), + 0x0013: ('ISOSpeedRequested', ), + 0x0016: ('PhotoCornerCoordinates', ), + 0x0018: ('FlashBracketCompensationApplied', ), + 0x0019: ('AEBracketCompensationApplied', ), + 0x0080: ('ImageAdjustment', ), + 0x0081: ('ToneCompensation', ), + 0x0082: ('AuxiliaryLens', ), + 0x0083: ('LensType', ), + 0x0084: ('LensMinMaxFocalMaxAperture', ), + 0x0085: ('ManualFocusDistance', ), + 0x0086: ('DigitalZoomFactor', ), + 0x0088: ('AFFocusPosition', + {0x0000: 'Center', + 0x0100: 'Top', + 0x0200: 'Bottom', + 0x0300: 'Left', + 0x0400: 'Right'}), + 0x0089: ('BracketingMode', + {0x00: 'Single frame, no bracketing', + 0x01: 'Continuous, no bracketing', + 0x02: 'Timer, no bracketing', + 0x10: 'Single frame, exposure bracketing', + 0x11: 'Continuous, exposure bracketing', + 0x12: 'Timer, exposure bracketing', + 0x40: 'Single frame, white balance bracketing', + 0x41: 'Continuous, white balance bracketing', + 0x42: 'Timer, white balance bracketing'}), + 0x008D: ('ColorMode', ), + 0x008F: ('SceneMode?', ), + 0x0090: ('LightingType', ), + 0x0092: ('HueAdjustment', ), + 0x0094: ('Saturation', + {-3: 'B&W', + -2: '-2', + -1: '-1', + 0: '0', + 1: '1', + 2: '2'}), + 0x0095: ('NoiseReduction', ), + 0x00A7: ('TotalShutterReleases', ), + 0x00A9: ('ImageOptimization', ), + 0x00AA: ('Saturation', ), + 0x00AB: ('DigitalVariProgram', ), + 0x0010: ('DataDump', ) + } + +MAKERNOTE_NIKON_OLDER_TAGS={ + 0x0003: ('Quality', + {1: 'VGA Basic', + 2: 'VGA Normal', + 3: 'VGA Fine', + 4: 'SXGA Basic', + 5: 'SXGA Normal', + 6: 'SXGA Fine'}), + 0x0004: ('ColorMode', + {1: 'Color', + 2: 'Monochrome'}), + 0x0005: ('ImageAdjustment', + {0: 'Normal', + 1: 'Bright+', + 2: 'Bright-', + 3: 'Contrast+', + 4: 'Contrast-'}), + 0x0006: ('CCDSpeed', + {0: 'ISO 80', + 2: 'ISO 160', + 4: 'ISO 320', + 5: 'ISO 100'}), + 0x0007: ('WhiteBalance', + {0: 'Auto', + 1: 'Preset', + 2: 'Daylight', + 3: 'Incandescent', + 4: 'Fluorescent', + 5: 'Cloudy', + 6: 'Speed Light'}) + } + +# decode Olympus SpecialMode tag in MakerNote +def olympus_special_mode(v): + a={ + 0: 'Normal', + 1: 'Unknown', + 2: 'Fast', + 3: 'Panorama'} + b={ + 0: 'Non-panoramic', + 1: 'Left to right', + 2: 'Right to left', + 3: 'Bottom to top', + 4: 'Top to bottom'} + return '%s - sequence %d - %s' % (a[v[0]], v[1], b[v[2]]) + +MAKERNOTE_OLYMPUS_TAGS={ + # ah HAH! those sneeeeeaky bastids! this is how they get past the fact + # that a JPEG thumbnail is not allowed in an uncompressed TIFF file + 0x0100: ('JPEGThumbnail', ), + 0x0200: ('SpecialMode', olympus_special_mode), + 0x0201: ('JPEGQual', + {1: 'SQ', + 2: 'HQ', + 3: 'SHQ'}), + 0x0202: ('Macro', + {0: 'Normal', + 1: 'Macro'}), + 0x0204: ('DigitalZoom', ), + 0x0207: ('SoftwareRelease', ), + 0x0208: ('PictureInfo', ), + # print as string + 0x0209: ('CameraID', lambda x: ''.join(map(chr, x))), + 0x0F00: ('DataDump', ) + } + +MAKERNOTE_CASIO_TAGS={ + 0x0001: ('RecordingMode', + {1: 'Single Shutter', + 2: 'Panorama', + 3: 'Night Scene', + 4: 'Portrait', + 5: 'Landscape'}), + 0x0002: ('Quality', + {1: 'Economy', + 2: 'Normal', + 3: 'Fine'}), + 0x0003: ('FocusingMode', + {2: 'Macro', + 3: 'Auto Focus', + 4: 'Manual Focus', + 5: 'Infinity'}), + 0x0004: ('FlashMode', + {1: 'Auto', + 2: 'On', + 3: 'Off', + 4: 'Red Eye Reduction'}), + 0x0005: ('FlashIntensity', + {11: 'Weak', + 13: 'Normal', + 15: 'Strong'}), + 0x0006: ('Object Distance', ), + 0x0007: ('WhiteBalance', + {1: 'Auto', + 2: 'Tungsten', + 3: 'Daylight', + 4: 'Fluorescent', + 5: 'Shade', + 129: 'Manual'}), + 0x000B: ('Sharpness', + {0: 'Normal', + 1: 'Soft', + 2: 'Hard'}), + 0x000C: ('Contrast', + {0: 'Normal', + 1: 'Low', + 2: 'High'}), + 0x000D: ('Saturation', + {0: 'Normal', + 1: 'Low', + 2: 'High'}), + 0x0014: ('CCDSpeed', + {64: 'Normal', + 80: 'Normal', + 100: 'High', + 125: '+1.0', + 244: '+3.0', + 250: '+2.0',}) + } + +MAKERNOTE_FUJIFILM_TAGS={ + 0x0000: ('NoteVersion', lambda x: ''.join(map(chr, x))), + 0x1000: ('Quality', ), + 0x1001: ('Sharpness', + {1: 'Soft', + 2: 'Soft', + 3: 'Normal', + 4: 'Hard', + 5: 'Hard'}), + 0x1002: ('WhiteBalance', + {0: 'Auto', + 256: 'Daylight', + 512: 'Cloudy', + 768: 'DaylightColor-Fluorescent', + 769: 'DaywhiteColor-Fluorescent', + 770: 'White-Fluorescent', + 1024: 'Incandescent', + 3840: 'Custom'}), + 0x1003: ('Color', + {0: 'Normal', + 256: 'High', + 512: 'Low'}), + 0x1004: ('Tone', + {0: 'Normal', + 256: 'High', + 512: 'Low'}), + 0x1010: ('FlashMode', + {0: 'Auto', + 1: 'On', + 2: 'Off', + 3: 'Red Eye Reduction'}), + 0x1011: ('FlashStrength', ), + 0x1020: ('Macro', + {0: 'Off', + 1: 'On'}), + 0x1021: ('FocusMode', + {0: 'Auto', + 1: 'Manual'}), + 0x1030: ('SlowSync', + {0: 'Off', + 1: 'On'}), + 0x1031: ('PictureMode', + {0: 'Auto', + 1: 'Portrait', + 2: 'Landscape', + 4: 'Sports', + 5: 'Night', + 6: 'Program AE', + 256: 'Aperture Priority AE', + 512: 'Shutter Priority AE', + 768: 'Manual Exposure'}), + 0x1100: ('MotorOrBracket', + {0: 'Off', + 1: 'On'}), + 0x1300: ('BlurWarning', + {0: 'Off', + 1: 'On'}), + 0x1301: ('FocusWarning', + {0: 'Off', + 1: 'On'}), + 0x1302: ('AEWarning', + {0: 'Off', + 1: 'On'}) + } + +MAKERNOTE_CANON_TAGS={ + 0x0006: ('ImageType', ), + 0x0007: ('FirmwareVersion', ), + 0x0008: ('ImageNumber', ), + 0x0009: ('OwnerName', ) + } + +# see http://www.burren.cx/david/canon.html by David Burren +# this is in element offset, name, optional value dictionary format +MAKERNOTE_CANON_TAG_0x001={ + 1: ('Macromode', + {1: 'Macro', + 2: 'Normal'}), + 2: ('SelfTimer', ), + 3: ('Quality', + {2: 'Normal', + 3: 'Fine', + 5: 'Superfine'}), + 4: ('FlashMode', + {0: 'Flash Not Fired', + 1: 'Auto', + 2: 'On', + 3: 'Red-Eye Reduction', + 4: 'Slow Synchro', + 5: 'Auto + Red-Eye Reduction', + 6: 'On + Red-Eye Reduction', + 16: 'external flash'}), + 5: ('ContinuousDriveMode', + {0: 'Single Or Timer', + 1: 'Continuous'}), + 7: ('FocusMode', + {0: 'One-Shot', + 1: 'AI Servo', + 2: 'AI Focus', + 3: 'MF', + 4: 'Single', + 5: 'Continuous', + 6: 'MF'}), + 10: ('ImageSize', + {0: 'Large', + 1: 'Medium', + 2: 'Small'}), + 11: ('EasyShootingMode', + {0: 'Full Auto', + 1: 'Manual', + 2: 'Landscape', + 3: 'Fast Shutter', + 4: 'Slow Shutter', + 5: 'Night', + 6: 'B&W', + 7: 'Sepia', + 8: 'Portrait', + 9: 'Sports', + 10: 'Macro/Close-Up', + 11: 'Pan Focus'}), + 12: ('DigitalZoom', + {0: 'None', + 1: '2x', + 2: '4x'}), + 13: ('Contrast', + {0xFFFF: 'Low', + 0: 'Normal', + 1: 'High'}), + 14: ('Saturation', + {0xFFFF: 'Low', + 0: 'Normal', + 1: 'High'}), + 15: ('Sharpness', + {0xFFFF: 'Low', + 0: 'Normal', + 1: 'High'}), + 16: ('ISO', + {0: 'See ISOSpeedRatings Tag', + 15: 'Auto', + 16: '50', + 17: '100', + 18: '200', + 19: '400'}), + 17: ('MeteringMode', + {3: 'Evaluative', + 4: 'Partial', + 5: 'Center-weighted'}), + 18: ('FocusType', + {0: 'Manual', + 1: 'Auto', + 3: 'Close-Up (Macro)', + 8: 'Locked (Pan Mode)'}), + 19: ('AFPointSelected', + {0x3000: 'None (MF)', + 0x3001: 'Auto-Selected', + 0x3002: 'Right', + 0x3003: 'Center', + 0x3004: 'Left'}), + 20: ('ExposureMode', + {0: 'Easy Shooting', + 1: 'Program', + 2: 'Tv-priority', + 3: 'Av-priority', + 4: 'Manual', + 5: 'A-DEP'}), + 23: ('LongFocalLengthOfLensInFocalUnits', ), + 24: ('ShortFocalLengthOfLensInFocalUnits', ), + 25: ('FocalUnitsPerMM', ), + 28: ('FlashActivity', + {0: 'Did Not Fire', + 1: 'Fired'}), + 29: ('FlashDetails', + {14: 'External E-TTL', + 13: 'Internal Flash', + 11: 'FP Sync Used', + 7: '2nd("Rear")-Curtain Sync Used', + 4: 'FP Sync Enabled'}), + 32: ('FocusMode', + {0: 'Single', + 1: 'Continuous'}) + } + +MAKERNOTE_CANON_TAG_0x004={ + 7: ('WhiteBalance', + {0: 'Auto', + 1: 'Sunny', + 2: 'Cloudy', + 3: 'Tungsten', + 4: 'Fluorescent', + 5: 'Flash', + 6: 'Custom'}), + 9: ('SequenceNumber', ), + 14: ('AFPointUsed', ), + 15: ('FlashBias', + {0XFFC0: '-2 EV', + 0XFFCC: '-1.67 EV', + 0XFFD0: '-1.50 EV', + 0XFFD4: '-1.33 EV', + 0XFFE0: '-1 EV', + 0XFFEC: '-0.67 EV', + 0XFFF0: '-0.50 EV', + 0XFFF4: '-0.33 EV', + 0X0000: '0 EV', + 0X000C: '0.33 EV', + 0X0010: '0.50 EV', + 0X0014: '0.67 EV', + 0X0020: '1 EV', + 0X002C: '1.33 EV', + 0X0030: '1.50 EV', + 0X0034: '1.67 EV', + 0X0040: '2 EV'}), + 19: ('SubjectDistance', ) + } + +# extract multibyte integer in Motorola format (little endian) +def s2n_motorola(str): + x=0 + for c in str: + x=(x << 8) | ord(c) + return x + +# extract multibyte integer in Intel format (big endian) +def s2n_intel(str): + x=0 + y=0L + for c in str: + x=x | (ord(c) << y) + y=y+8 + return x + +# ratio object that eventually will be able to reduce itself to lowest +# common denominator for printing +def gcd(a, b): + if b == 0: + return a + else: + return gcd(b, a % b) + +class Ratio: + def __init__(self, num, den): + self.num=num + self.den=den + + def __repr__(self): + self.reduce() + if self.den == 1: + return str(self.num) + return '%d/%d' % (self.num, self.den) + + def reduce(self): + div=gcd(self.num, self.den) + if div > 1: + self.num=self.num/div + self.den=self.den/div + +# for ease of dealing with tags +class IFD_Tag: + def __init__(self, printable, tag, field_type, values, field_offset, + field_length): + # printable version of data + self.printable=printable + # tag ID number + self.tag=tag + # field type as index into FIELD_TYPES + self.field_type=field_type + # offset of start of field in bytes from beginning of IFD + self.field_offset=field_offset + # length of data field in bytes + self.field_length=field_length + # either a string or array of data items + self.values=values + + def __str__(self): + return self.printable + + def __repr__(self): + return '(0x%04X) %s=%s @ %d' % (self.tag, + FIELD_TYPES[self.field_type][2], + self.printable, + self.field_offset) + +# class that handles an EXIF header +class EXIF_header: + def __init__(self, file, endian, offset, fake_exif, debug=0): + self.file=file + self.endian=endian + self.offset=offset + self.fake_exif=fake_exif + self.debug=debug + self.tags={} + + # convert slice to integer, based on sign and endian flags + # usually this offset is assumed to be relative to the beginning of the + # start of the EXIF information. For some cameras that use relative tags, + # this offset may be relative to some other starting point. + def s2n(self, offset, length, signed=0): + self.file.seek(self.offset+offset) + slice=self.file.read(length) + if self.endian == 'I': + val=s2n_intel(slice) + else: + val=s2n_motorola(slice) + # Sign extension ? + if signed: + msb=1L << (8*length-1) + if val & msb: + val=val-(msb << 1) + return val + + # convert offset to string + def n2s(self, offset, length): + s='' + for i in range(length): + if self.endian == 'I': + s=s+chr(offset & 0xFF) + else: + s=chr(offset & 0xFF)+s + offset=offset >> 8 + return s + + # return first IFD + def first_IFD(self): + return self.s2n(4, 4) + + # return pointer to next IFD + def next_IFD(self, ifd): + entries=self.s2n(ifd, 2) + return self.s2n(ifd+2+12*entries, 4) + + # return list of IFDs in header + def list_IFDs(self): + i=self.first_IFD() + a=[] + while i: + a.append(i) + i=self.next_IFD(i) + return a + + # return list of entries in this IFD + def dump_IFD(self, ifd, ifd_name, dict=EXIF_TAGS, relative=0): + entries=self.s2n(ifd, 2) + for i in range(entries): + # entry is index of start of this IFD in the file + entry=ifd+2+12*i + tag=self.s2n(entry, 2) + # get tag name. We do it early to make debugging easier + tag_entry=dict.get(tag) + if tag_entry: + tag_name=tag_entry[0] + else: + tag_name='Tag 0x%04X' % tag + field_type=self.s2n(entry+2, 2) + if not 0 < field_type < len(FIELD_TYPES): + # unknown field type + raise ValueError, \ + 'unknown type %d in tag 0x%04X' % (field_type, tag) + typelen=FIELD_TYPES[field_type][0] + count=self.s2n(entry+4, 4) + offset=entry+8 + if count*typelen > 4: + # offset is not the value; it's a pointer to the value + # if relative we set things up so s2n will seek to the right + # place when it adds self.offset. Note that this 'relative' + # is for the Nikon type 3 makernote. Other cameras may use + # other relative offsets, which would have to be computed here + # slightly differently. + if relative: + tmp_offset=self.s2n(offset, 4) + offset=tmp_offset+ifd-self.offset+4 + if self.fake_exif: + offset=offset+18 + else: + offset=self.s2n(offset, 4) + field_offset=offset + if field_type == 2: + # special case: null-terminated ASCII string + if count != 0: + self.file.seek(self.offset+offset) + values=self.file.read(count) + values=values.strip().replace('\x00','') + else: + values='' + else: + values=[] + signed=(field_type in [6, 8, 9, 10]) + for j in range(count): + if field_type in (5, 10): + # a ratio + value_j=Ratio(self.s2n(offset, 4, signed), + self.s2n(offset+4, 4, signed)) + else: + value_j=self.s2n(offset, typelen, signed) + values.append(value_j) + offset=offset+typelen + # now "values" is either a string or an array + if count == 1 and field_type != 2: + printable=str(values[0]) + else: + printable=str(values) + # compute printable version of values + if tag_entry: + if len(tag_entry) != 1: + # optional 2nd tag element is present + if callable(tag_entry[1]): + # call mapping function + printable=tag_entry[1](values) + else: + printable='' + for i in values: + # use lookup table for this tag + printable+=tag_entry[1].get(i, repr(i)) + self.tags[ifd_name+' '+tag_name]=IFD_Tag(printable, tag, + field_type, + values, field_offset, + count*typelen) + if self.debug: + print ' debug: %s: %s' % (tag_name, + repr(self.tags[ifd_name+' '+tag_name])) + + # extract uncompressed TIFF thumbnail (like pulling teeth) + # we take advantage of the pre-existing layout in the thumbnail IFD as + # much as possible + def extract_TIFF_thumbnail(self, thumb_ifd): + entries=self.s2n(thumb_ifd, 2) + # this is header plus offset to IFD ... + if self.endian == 'M': + tiff='MM\x00*\x00\x00\x00\x08' + else: + tiff='II*\x00\x08\x00\x00\x00' + # ... plus thumbnail IFD data plus a null "next IFD" pointer + self.file.seek(self.offset+thumb_ifd) + tiff+=self.file.read(entries*12+2)+'\x00\x00\x00\x00' + + # fix up large value offset pointers into data area + for i in range(entries): + entry=thumb_ifd+2+12*i + tag=self.s2n(entry, 2) + field_type=self.s2n(entry+2, 2) + typelen=FIELD_TYPES[field_type][0] + count=self.s2n(entry+4, 4) + oldoff=self.s2n(entry+8, 4) + # start of the 4-byte pointer area in entry + ptr=i*12+18 + # remember strip offsets location + if tag == 0x0111: + strip_off=ptr + strip_len=count*typelen + # is it in the data area? + if count*typelen > 4: + # update offset pointer (nasty "strings are immutable" crap) + # should be able to say "tiff[ptr:ptr+4]=newoff" + newoff=len(tiff) + tiff=tiff[:ptr]+self.n2s(newoff, 4)+tiff[ptr+4:] + # remember strip offsets location + if tag == 0x0111: + strip_off=newoff + strip_len=4 + # get original data and store it + self.file.seek(self.offset+oldoff) + tiff+=self.file.read(count*typelen) + + # add pixel strips and update strip offset info + old_offsets=self.tags['Thumbnail StripOffsets'].values + old_counts=self.tags['Thumbnail StripByteCounts'].values + for i in range(len(old_offsets)): + # update offset pointer (more nasty "strings are immutable" crap) + offset=self.n2s(len(tiff), strip_len) + tiff=tiff[:strip_off]+offset+tiff[strip_off+strip_len:] + strip_off+=strip_len + # add pixel strip to end + self.file.seek(self.offset+old_offsets[i]) + tiff+=self.file.read(old_counts[i]) + + self.tags['TIFFThumbnail']=tiff + + # decode all the camera-specific MakerNote formats + + # Note is the data that comprises this MakerNote. The MakerNote will + # likely have pointers in it that point to other parts of the file. We'll + # use self.offset as the starting point for most of those pointers, since + # they are relative to the beginning of the file. + # + # If the MakerNote is in a newer format, it may use relative addressing + # within the MakerNote. In that case we'll use relative addresses for the + # pointers. + # + # As an aside: it's not just to be annoying that the manufacturers use + # relative offsets. It's so that if the makernote has to be moved by the + # picture software all of the offsets don't have to be adjusted. Overall, + # this is probably the right strategy for makernotes, though the spec is + # ambiguous. (The spec does not appear to imagine that makernotes would + # follow EXIF format internally. Once they did, it's ambiguous whether + # the offsets should be from the header at the start of all the EXIF info, + # or from the header at the start of the makernote.) + def decode_maker_note(self): + note=self.tags['EXIF MakerNote'] + make=self.tags['Image Make'].printable + model=self.tags['Image Model'].printable + + # Nikon + # The maker note usually starts with the word Nikon, followed by the + # type of the makernote (1 or 2, as a short). If the word Nikon is + # not at the start of the makernote, it's probably type 2, since some + # cameras work that way. + if make in ('NIKON', 'NIKON CORPORATION'): + if note.values[0:7] == [78, 105, 107, 111, 110, 00, 01]: + if self.debug: + print "Looks like a type 1 Nikon MakerNote." + self.dump_IFD(note.field_offset+8, 'MakerNote', + dict=MAKERNOTE_NIKON_OLDER_TAGS) + elif note.values[0:7] == [78, 105, 107, 111, 110, 00, 02]: + if self.debug: + print "Looks like a labeled type 2 Nikon MakerNote" + if note.values[12:14] != [0, 42] and note.values[12:14] != [42L, 0L]: + raise ValueError, "Missing marker tag '42' in MakerNote." + # skip the Makernote label and the TIFF header + self.dump_IFD(note.field_offset+10+8, 'MakerNote', + dict=MAKERNOTE_NIKON_NEWER_TAGS, relative=1) + else: + # E99x or D1 + if self.debug: + print "Looks like an unlabeled type 2 Nikon MakerNote" + self.dump_IFD(note.field_offset, 'MakerNote', + dict=MAKERNOTE_NIKON_NEWER_TAGS) + return + + # Olympus + if make[:7] == 'OLYMPUS': + self.dump_IFD(note.field_offset+8, 'MakerNote', + dict=MAKERNOTE_OLYMPUS_TAGS) + return + + # Casio + if make == 'Casio': + self.dump_IFD(note.field_offset, 'MakerNote', + dict=MAKERNOTE_CASIO_TAGS) + return + + # Fujifilm + if make == 'FUJIFILM': + # bug: everything else is "Motorola" endian, but the MakerNote + # is "Intel" endian + endian=self.endian + self.endian='I' + # bug: IFD offsets are from beginning of MakerNote, not + # beginning of file header + offset=self.offset + self.offset+=note.field_offset + # process note with bogus values (note is actually at offset 12) + self.dump_IFD(12, 'MakerNote', dict=MAKERNOTE_FUJIFILM_TAGS) + # reset to correct values + self.endian=endian + self.offset=offset + return + + # Canon + if make == 'Canon': + self.dump_IFD(note.field_offset, 'MakerNote', + dict=MAKERNOTE_CANON_TAGS) + for i in (('MakerNote Tag 0x0001', MAKERNOTE_CANON_TAG_0x001), + ('MakerNote Tag 0x0004', MAKERNOTE_CANON_TAG_0x004)): + self.canon_decode_tag(self.tags[i[0]].values, i[1]) + return + + # decode Canon MakerNote tag based on offset within tag + # see http://www.burren.cx/david/canon.html by David Burren + def canon_decode_tag(self, value, dict): + for i in range(1, len(value)): + x=dict.get(i, ('Unknown', )) + if self.debug: + print i, x + name=x[0] + if len(x) > 1: + val=x[1].get(value[i], 'Unknown') + else: + val=value[i] + # it's not a real IFD Tag but we fake one to make everybody + # happy. this will have a "proprietary" type + self.tags['MakerNote '+name]=IFD_Tag(str(val), None, 0, None, + None, None) + +# process an image file (expects an open file object) +# this is the function that has to deal with all the arbitrary nasty bits +# of the EXIF standard +def process_file(file, debug=0): + # determine whether it's a JPEG or TIFF + data=file.read(12) + if data[0:4] in ['II*\x00', 'MM\x00*']: + # it's a TIFF file + file.seek(0) + endian=file.read(1) + file.read(1) + offset=0 + elif data[0:2] == '\xFF\xD8': + # it's a JPEG file + # skip JFIF style header(s) + fake_exif=0 + while data[2] == '\xFF' and data[6:10] in ('JFIF', 'JFXX', 'OLYM'): + length=ord(data[4])*256+ord(data[5]) + file.read(length-8) + # fake an EXIF beginning of file + data='\xFF\x00'+file.read(10) + fake_exif=1 + if data[2] == '\xFF' and data[6:10] == 'Exif': + # detected EXIF header + offset=file.tell() + endian=file.read(1) + else: + # no EXIF information + return {} + else: + # file format not recognized + return {} + + # deal with the EXIF info we found + if debug: + print {'I': 'Intel', 'M': 'Motorola'}[endian], 'format' + hdr=EXIF_header(file, endian, offset, fake_exif, debug) + ifd_list=hdr.list_IFDs() + ctr=0 + for i in ifd_list: + if ctr == 0: + IFD_name='Image' + elif ctr == 1: + IFD_name='Thumbnail' + thumb_ifd=i + else: + IFD_name='IFD %d' % ctr + if debug: + print ' IFD %d (%s) at offset %d:' % (ctr, IFD_name, i) + hdr.dump_IFD(i, IFD_name) + # EXIF IFD + exif_off=hdr.tags.get(IFD_name+' ExifOffset') + if exif_off: + if debug: + print ' EXIF SubIFD at offset %d:' % exif_off.values[0] + hdr.dump_IFD(exif_off.values[0], 'EXIF') + # Interoperability IFD contained in EXIF IFD + intr_off=hdr.tags.get('EXIF SubIFD InteroperabilityOffset') + if intr_off: + if debug: + print ' EXIF Interoperability SubSubIFD at offset %d:' \ + % intr_off.values[0] + hdr.dump_IFD(intr_off.values[0], 'EXIF Interoperability', + dict=INTR_TAGS) + # GPS IFD + gps_off=hdr.tags.get(IFD_name+' GPSInfo') + if gps_off: + if debug: + print ' GPS SubIFD at offset %d:' % gps_off.values[0] + hdr.dump_IFD(gps_off.values[0], 'GPS', dict=GPS_TAGS) + ctr+=1 + + # extract uncompressed TIFF thumbnail + thumb=hdr.tags.get('Thumbnail Compression') + if thumb and thumb.printable == 'Uncompressed TIFF': + hdr.extract_TIFF_thumbnail(thumb_ifd) + + # JPEG thumbnail (thankfully the JPEG data is stored as a unit) + thumb_off=hdr.tags.get('Thumbnail JPEGInterchangeFormat') + if thumb_off: + file.seek(offset+thumb_off.values[0]) + size=hdr.tags['Thumbnail JPEGInterchangeFormatLength'].values[0] + hdr.tags['JPEGThumbnail']=file.read(size) + + # deal with MakerNote contained in EXIF IFD + if hdr.tags.has_key('EXIF MakerNote'): + hdr.decode_maker_note() + + # Sometimes in a TIFF file, a JPEG thumbnail is hidden in the MakerNote + # since it's not allowed in a uncompressed TIFF IFD + if not hdr.tags.has_key('JPEGThumbnail'): + thumb_off=hdr.tags.get('MakerNote JPEGThumbnail') + if thumb_off: + file.seek(offset+thumb_off.values[0]) + hdr.tags['JPEGThumbnail']=file.read(thumb_off.field_length) + + return hdr.tags + +# library test/debug function (dump given files) +if __name__ == '__main__': + import sys + + if len(sys.argv) < 2: + print 'Usage: %s files...\n' % sys.argv[0] + sys.exit(0) + + for filename in sys.argv[1:]: + try: + file=open(filename, 'rb') + except: + print filename, 'unreadable' + print + continue + print filename+':' + # data=process_file(file, 1) # with debug info + data=process_file(file) + if not data: + print 'No EXIF information found' + continue + + x=data.keys() + x.sort() + for i in x: + if i in ('JPEGThumbnail', 'TIFFThumbnail'): + continue + try: + print ' %s (%s): %s' % \ + (i, FIELD_TYPES[data[i].field_type][2], data[i].printable) + except: + print 'error', i, '"', data[i], '"' + if data.has_key('JPEGThumbnail'): + print 'File has JPEG thumbnail' + print diff --git a/wiki/parser/Gallery2.py b/wiki/parser/Gallery2.py new file mode 100644 index 00000000..ee227b9a --- /dev/null +++ b/wiki/parser/Gallery2.py @@ -0,0 +1,890 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - Gallery2 parser + + PURPOSE: + This parser is used to visualize a couple of images as a thumbnail gallery. + Optional a description of an image could be added including WikiName. + On default the image name and it's creation date is shown. + If you click on a thumbnail you get the webnails shown. By a menue you are able to toggle between the slides. + + CALLING SEQUENCE: + {{{ + #!Gallery2 [columns=columns],[filter=filter],[mode=mode], + [show_text=show_text],[show_date=show_date], [show_tools=show_tools], + [sort_by_name=sort_by_name],[sort_by_date=sort_by_date], [sort_by_alias=sort_by_alias], + [reverse_sort=reverse_sort], + [only_items=only_items],[template_itemlist=template_itemlist], + [album=album],[album_name=album_name],[front_image=front_image], + [thumbnail_width=thumbnail_width],[webnail_width=webnail_width],[text_width=text_width], + [image_for_webnail=image_for_webnail], + [border_thick=border_thick],[renew=renew],[help=help] + * [image1.jpg alias] + * [image2.jpg alias] + }}} + + KEYWORD PARAMETERS: + columns: number of columns for thumbnails + filter: regex to select images + show_text: default is 1 description is shown + any other means no description + show_date: default is 1 date info from exif header if available is shown + show_tools: default is 1 icon toolbar is show any other disables this + sort_by_name: default is 1, the images are sorted by name, but not if only_items is 1 + sort_by_date: default is 0, if set to 1 the images are sorted to the modification time + sort_by_alias default is 0, if set to 1 and only_items set to 1 it is used to order the images by the alias name + reverse_sort: default is 0, if set to 1 the file list is reversed + any other means no description + mode: default is 1 this means description below the image + any other number means description right of image + only_items: default is 0 if it is set to 1 only images which are described in listitem are shown + dependend on the order of the items + template_itemlist: default is 0, if set to 1 an item list is shown which could be copied into the script. + album: default is 0 if set to 1 only the first image of a series is shown but slideshow over all images + album_name: useful for album. default is 'album' use it as short name for the album. + front_image: Useful for album. default is ''. The first image is shown in front of the album and slideshow. + If set to an existing image name this is shown in front of album and slideshow. + The slide show could start by this somewhere. + border_thick: default is 1 this is the thickness in pixeln of the outer frame + renew: default is 0 if set to 1 then all selected thumbnails_* and webnails_* removed. + Afterwards they are new created. + thumbnail_width: default is 128 + webnail_width: default is 640 + text_width: default is 140 + image_for_webnail default is 0 if set to 1 then the image is shown as preview and not the webnail + help: default is 0 if set a copy of the CALLING SEQUENCE is shown, + (there are some new ideas around to show help to an user so this will be later replaced) + + OPTIONAL INPUTS: + itemlist : if it is used and only_items is 1 then only the images in this list are ahown. + The alias text is used as description of the image instead of the file name + + + EXAMPLE: += GalleryTest = + +== all images shown, one is decribed == +{{{ +{ { { +#!Gallery2 +* [100_1185.JPG Bremen, SpaceCenter] +} } } +}}} + +Result: [[BR]] + {{{ +#!Gallery2 +* [100_1185.JPG Bremen, SpaceCenter] +}}} + +== only thumbnails and only_items == +{{{ +{ { { +#!Gallery2 show_text=0,show_tools=0,show_date=0,columns=2,only_items=1 + * [100_1185.JPG Bremen, SpaceCenter] + * [100_1194.JPG Bremen] +} } } +}}} + +Result: [[BR]] + {{{ +#!Gallery2 show_text=0,show_tools=0,show_date=0,columns=2,only_items=1 + * [100_1185.JPG Bremen, SpaceCenter] + * [100_1194.JPG Bremen] +}}} + +== only_items by two columns and text right == + +{{{ +{ { { +#!Gallery2 mode=2,columns=2,only_items=1 + * [100_1185.JPG Bremen, SpaceCenter] + * [100_1194.JPG Bremen] +} } } +}}} + +Result: [[BR]] + {{{ +#!Gallery2 mode=2,columns=2,only_items=1 + * [100_1185.JPG Bremen, SpaceCenter] + * [100_1194.JPG Bremen, behind SpaceCenter] +}}} + +---- + +== only_items by two columns, date supressed == + +{{{ +{ { { +#!Gallery2 columns=2,only_items=1,show_date=0 + * [100_1185.JPG Bremen, SpaceCenter] + * [100_1194.JPG Bremen, behind SpaceCenter] +} } } +}}} + +Result: [[BR]] + {{{ +#!Gallery2 columns=2,only_items=1,show_date=0 + * [100_1185.JPG Bremen, SpaceCenter] + * [100_1194.JPG Bremen, behind SpaceCenter] +}}} + + +== filter regex used, mode 2, icons and date supressed, one column and border_thick=5 == +{{{ +{ { { +#!Gallery2 columns=1,filter=100_118[0-5],mode=2,show_date=0,show_tools=0,border_thick=5 +} } } +}}} + +Result: [[BR]] + {{{ +#!Gallery2 columns=1,filter=100_118[0-7],mode=2,show_date=0,show_tools=0,border_thick=5 +}}} + +== other macro calls == +{{{ +{ { { +#!Gallery2 only_items=1,show_date=0 + * [100_1189.JPG [[MiniPage(||Bremen||SpaceCenter||\n|| ||SpaceJump||)]]] +} } } +}}} + +Result: [[BR]] + {{{ +#!Gallery2 only_items=1,show_date=0 + * [100_1189.JPG [[MiniPage(||Bremen||SpaceCenter||\n|| ||SpaceJump||)]]] +}}} + +== renew means always new thumbnails and webnails of selection == +{{{ +{ { { +#!Gallery2 only_items=1,show_date=0,show_tools=0,renew=1 + * [100_1189.JPG [[MiniPage(||["Bremen"]||SpaceCenter||\n|| ||SpaceJump||)]]] +} } } +}}} + +Result: [[BR]] + {{{ +#!Gallery2 only_items=1,show_date=0,renew=1 + * [100_1189.JPG [[MiniPage(||["Bremen"]||SpaceCenter||\n|| ||SpaceJump||)]]] +}}} + +== template_itemlist == +{{{ +{ { { +#!Gallery2 template_itemlist=1 +* [100_1185.JPG Bremen, SpaceCenter] +} } } +}}} + +Result: [[BR]] + {{{ +#!Gallery2 template_itemlist=1 +* [100_1185.JPG Bremen, SpaceCenter] +}}} + +== help to show Calling Sequence == + {{{ +{ { { +#!Gallery2 help=1 +} } } +}}} + +Result: [[BR]] +{{{ +#!Gallery2 help=1 +}}} + + + PROCEDURE: + Download some images to a page and start with the examples. + Aliasing of the filenames are done by adding an itemlist, see example. + + This routine requires the PIL (Python Imaging Library). + And the EXIF routine from http://home.cfl.rr.com/genecash/digital_camera.html + + + At the moment I have added the EXIF routine to the parsers dir. + It's not the best place but during developing it is nice to have it there + If you put it to another place you have to change the line + from MoinMoin.parser import EXIF too. + + This routine requires the Action macro gallery2Image which is used to rotate or delete a selected image. + Only users which have the rights to delete are able to execute this action macro. + The icons of these are only shown if you have enough rights. + + The gallery2image macro does not take care on the EXIF header. This is lost by rotating. + If a file is deleted by this macro it is moved to a bak file. + + Please remove the Version number from the code! + + RESTRICTIONS: + The movie mode is not implemented at the moment. The implementation will be done in the action macro. + + + If you rotate an image at the moment the exif is destroyed. PIL ignores the exif header. + This is not a quite big problem normally files with an EXIF header are right rotated. + + Required Images: + I have put them to wiki/modern/img/ dir. The icons were created by me. License: GPL + + attachment:to_bak.png + attachment:to_left.png + attachment:to_right.png + attachment:to_slide.png + attachment:to_full.png + + HISTORY: + While recognizing how to write MiniPage I got the idea to write a Gallery Parser. + We have used in our wikis in the past the Gallery macro of SimonRyan. + I have tried to modify it a bit to change it for 1.3 but my python skills weren't enough + or it was easier to write it completly new. + So this one shows now a way how a Gallery could be used by the parser and an action Macro. + Probably it is a good example for others who like to know how to do this + + MODIFICATION HISTORY: + Version 1.3.3.-1 + @copyright: 2005 by Reimar Bauer (R.Bauer@fz-juelich.de) + @license: GNU GPL, see COPYING for details. + 2005-03-26: Version 1.3.3-2 keyword renew added + creation of thumbnails and webnails in two calls splitted + Version 1.3.3-3 bug fixed if itemlist is given to describe only some of the images + but only_items is not set to 1 + Example code changed + 2005-03-27: Version 1.3.3-4 Action macro added and the form to call it. User which have rights to delete + could use the functions of gallery2Image. + 2005-08-03: Version 1.3.3-5 theme path for icons corrected and a platform independent path joining + os.unlink removed as suggested by CraigJohnson + sort_by_name is default if not only_items is 1 + optional sort_by_date could be used + keyword template_itemlist added + keyword help added + extra frame by mode=2 removed + 2005-08-06: Version 1.3.5-6 slideshow mode added + keyword image_for_webnail added + 2005-08-13: Version 1.3.5-7 syntax changed from GET to POST + forms instead of links + filenames from images submitted to gallery2image too + new keyword sort_by_alias + internal code clean up + this version needs: gallery2image-1.3.5-5.py + 2005-08-14: Version 1.3.5-8 (TW) cleanup + 2005-08-14: Version 1.3.5-9 html code for tables changed + because of the ugly extra space of form elements + tag removed so now we use the page style + slide show action goes to right webnail now + this version needs: gallery2image-1.3.5-5.py + 2005-08-17: Version 1.3.5-10 html code separated in functions + structure of code changed, now you see the thumbnails after creation + bug removed if quote is given but file does not exist + 2005-09-02: Version 1.3.5-11 keyword album, album_name and front_image added + image urls changed to complete server url + + +""" +Dependencies = [] +from MoinMoin.action import AttachFile +from MoinMoin import wikiutil, config +from MoinMoin.Page import Page + +import os,string,re,Image,StringIO + +#from MoinMoin.parser import EXIF +import EXIF + +from MoinMoin.parser import wiki + +def show_tools_restricted(pagename,this_target,thumbnail_width,full,alias,target,exif_date,request): + if request.user.may.delete(pagename): + return tools_restricted_html(pagename,this_target,thumbnail_width,full,alias,target,exif_date,request) + else: + return '' + +def tools_restricted_html(pagename,this_target,thumbnail_width,full,alias,target,exif_date,request): + text=''' +
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + +
''' % { + "server" : 'https://'+request.server_name, + 'baseurl': request.getScriptname(), + "pagename":pagename, + "this_target":this_target} + return text + +def tools_html(pagename,this_image,thumbnail_width,full,alias,target,exif_date,request): + text=''' + + + + + + + + + %(show_tools_restricted)s + +
+ + + + +
+ + + + + + + +
''' % { + "server" : 'https://'+request.server_name, + 'baseurl': request.getScriptname(), + "pagename":pagename, + "thumbnail_width":thumbnail_width, + "full":full, + "alias":alias, + "exif_date":exif_date, + "target":target, + "this_target":this_image, + "show_tools_restricted":show_tools_restricted(pagename,this_image,thumbnail_width,full,alias,target,exif_date,request) + } + + return text + +def show_alias_mode2(show_alias,thumbnail_width,this_alias,text_width): + if show_alias == '1': + return ''' + + %(this_alias)s + ''' % { + "this_alias":this_alias, + "text_width":text_width} + else: + return '' + +def show_date_mode2(show_date,this_exif_date): + if show_date == '1': + return ''' + +

%(this_exif_date)s

+ ''' % { + "this_exif_date":this_exif_date } + else: + return ''; + +def show_tools_mode2(show_tools,pagename,this_target,thumbnail_width,full,alias,target,exif_date,request): + if show_tools == '1' : + return " %s " % tools_html(pagename,this_target,thumbnail_width,full,alias,target,exif_date,request) + else: + return '' + + + +def mode2_html(pagename,border_thick,width,thumbnail_width,text_width,full,this_image,alias,this_alias,exif_date,this_exif_date,target,this_target,submit,show_tools,show_date,show_alias,request): + text=''' + +
+ + + + + + + + + +
+ %(alias_html)s + + %(tools_html)s%(date_html)s'''% { + "server" : 'https://'+request.server_name, + "baseurl": request.getScriptname(), + "pagename":pagename, + "thumbnail_width":thumbnail_width, + "full":full, + "alias":alias, + "exif_date":exif_date, + "target":target, + "submit":submit, + "tools_html":show_tools_mode2(show_tools,pagename,this_image,thumbnail_width,full,alias,target,exif_date,request), + "date_html": show_date_mode2(show_date,this_exif_date), + "alias_html": show_alias_mode2(show_alias,thumbnail_width,this_alias,text_width) + } + + return text + +def show_tools_mode1(show_tools,pagename,this_image,thumbnail_width,full,alias,target,exif_date,request): + if show_tools == '1' : + text="%s " % tools_html(pagename,this_image,thumbnail_width,full,alias,target,exif_date,request) + else: + text='' + return text + +def show_date_mode1(show_date,this_exif_date): + if show_date == '1': + return ''' + + %(this_exif_date)s + ''' % { + "this_exif_date":this_exif_date} + else: + return '' + +def show_alias_mode1(show_alias,thumbnail_width,this_alias,text_width): + if show_alias == '1': + return ''' + + %(this_alias)s + ''' % { + "thumbnail_width": thumbnail_width, + "this_alias":this_alias} + else: + return '' + +def mode1_html(pagename,border_thick,width,thumbnail_width,text_width,full,this_image,alias,this_alias,exif_date,this_exif_date,target,this_target,submit, + show_tools,show_date,show_alias,request): + text=''' + + + + + + + %(alias_html)s + %(date_html)s + %(tools_html)s +
+ + + + + + + +
'''% { + "server" : 'https://'+request.server_name, + "baseurl": request.getScriptname() , + "pagename":pagename, + "full":full, + "alias":alias, + "exif_date":exif_date, + "target":target, + "submit":submit, + "thumbnail_width":thumbnail_width, + "tools_html": show_tools_mode1(show_tools,pagename,this_image,thumbnail_width,full,alias,target,exif_date,request), + "date_html":show_date_mode1(show_date,this_exif_date), + "alias_html": show_alias_mode1(show_alias,thumbnail_width,this_alias,text_width) + } + + return text + +def get_files(kw,path,files,quotes,request): + web=[] + full=[] + thumb=[] + exif_date=[] + img_type=[] + description=[] + + + ddict={} + n=len(quotes['image']) + if n > 0 : + i = 0 + for txt in quotes['image']: + ddict[txt]=quotes['alias'][i] + i += 1 + + for attfile in files: + # only files not thumb or webnails + if attfile.find('thumbnail_') == -1 and attfile.find('webnail_') == -1: + # only images + if wikiutil.isPicture(attfile): + description.append(ddict.get(attfile, attfile)) + full.append(attfile) + + if kw['image_for_webnail'] == '1': + webnail = attfile + else: + fname, ext = os.path.splitext(attfile) + if ext in ('.gif', '.png'): + img_type.append('PNG') + webnail = 'webnail_%s.png' % fname + thumbfile = 'thumbnail_%s.png' % fname + else: + img_type.append("JPEG") + webnail = 'webnail_%s.jpg' % fname + thumbfile = 'thumbnail_%s.jpg' % fname + + infile = os.path.join(path, attfile) + if os.path.exists(infile): + web.append(webnail) + thumb.append(thumbfile) + + f = open(infile, 'rb') + tags = EXIF.process_file(f) + if tags.has_key('EXIF DateTimeOriginal'): + date = str(tags['EXIF DateTimeOriginal']) + date = date.replace(':', '-', 2) + else: + date = '--' + exif_date.append(date) + f.close() + + return thumb,web,full,exif_date,img_type,description + +def to_htmltext(text): + text = text.split(' ') + text = ' '.join(text) + return text + +def to_wikiname(request,formatter,text): + ##taken from MiniPage + out=StringIO.StringIO() + request.redirect(out) + wikiizer = wiki.Parser(text.strip(),request) + wikiizer.format(formatter) + result=out.getvalue() + request.redirect() + del out + + return result.strip() + + +def get_quotes(self,formatter): + quotes = self.raw.split('\n') + quotes = [quote.strip() for quote in quotes] + quotes = [quote[2:] for quote in quotes if quote.startswith('* ')] + + + image=[] + text=[] + + for line in quotes: + im, na=line[1:-1].split(' ',1) + text.append(na.strip()) + image.append(im.strip()) + + return { + 'alias': text, + 'image': image, + } + + + +class Parser: + + def __init__(self, raw, request, **kw): + self.raw = raw + self.request = request + self.form = request.form + self._ = request.getText + self.kw = { + 'sort_by_date': '0', + 'sort_by_name': '1', + 'sort_by_alias': '0', + 'album': '0', + 'album_name': 'album', + 'front_image':'', + 'template_itemlist': '0', + 'reverse_sort': '0', + 'border_thick': '1', + 'columns': '4', + 'filter': '.', + 'mode': '1', + 'help': '0', + 'show_text': '1', + 'show_date': '1', + 'show_tools': '1', + 'only_items': '0', + 'image_for_webnail': '0', + 'renew': '0', + 'thumbnail_width': '128', + 'webnail_width': '640', + 'text_width': '140', + } + + + for arg in kw.get('format_args','').split(','): + + if arg.find('=') > -1: + key, value=arg.split('=') + self.kw[key]=wikiutil.escape(value, quote=1) + + + self.kw['width']=str((int(self.kw['thumbnail_width'])+int(self.kw['text_width']))) + + + def format(self, formatter): + kw=self.kw + Dict = {} + quotes=get_quotes(self,formatter) + current_pagename=formatter.page.page_name + attachment_path = AttachFile.getAttachDir(self.request, current_pagename, create=1) + + if kw['help'] == '1': + self.request.write(''' +
+{{{
+#!Gallery2 [columns=columns],[filter=filter],[mode=mode],
+ [show_text=show_text],[show_date=show_date], [show_tools=show_tools],
+ [sort_by_name=sort_by_name],[sort_by_date=sort_by_date],[sort_by_alias=sort_by_alias]
+ [reverse_sort=reverse_sort],
+ [only_items=only_items],[template_itemlist=template_itemlist],
+ [album=album],[album_name=album_name],[front_image=front_image],
+ [thumbnail_width=thumbnail_width],[webnail_width=webnail_width],[text_width=text_width],
+ [image_for_webnail=image_for_webnail],
+ [border_thick=border_thick],[renew=renew],[help=help]
+ * [image1.jpg alias]
+ * [image2.jpg alias]
+}}}
''') + return + + + if kw['only_items'] == '1': + all_files=quotes['image'] + result=[] + for attfile in all_files: + infile=os.path.join(attachment_path,attfile) + if os.path.exists(infile): + result.append(attfile) + all_files = result + + if kw['sort_by_alias'] == '1': + new_ordered_files=[] + alias_text=quotes['alias'] + + i=0 + for attfile in all_files: + infile=os.path.join(attachment_path,attfile) + ft_file=str(os.path.getmtime(infile))+os.tmpnam() + Dict[alias_text[i]]=attfile + i += 1 + + keys = Dict.keys() + keys.sort() + for txt in keys: + new_ordered_files.append(Dict[txt]) + + + all_files=new_ordered_files + Dict.clear() + + else: + all_files=os.listdir(attachment_path) + + result = [] + + for test in all_files: + if re.match(kw['filter'], test): + result.append(test) + all_files=result + + if not all_files: + self.request.write("

No matching image file found!

") + return + + + + if kw['sort_by_name'] == '1' and kw['only_items'] == '0': + all_files.sort() + + if kw['sort_by_date']=='1': + for attfile in all_files: + infile=os.path.join(attachment_path,attfile) + ft_file=str(os.path.getmtime(infile))+os.tmpnam() + Dict[ft_file]=attfile + + keys = Dict.keys() + keys.sort() + file_mdate=[] + for txt in keys: + file_mdate.append(Dict[txt]) + all_files=file_mdate + Dict.clear() + + if kw['reverse_sort']=='1': + all_files.reverse() + + + cells=[] + cell_name=[] + img=[] + + thumb, web, full, exif_date, imgtype, description = get_files(kw, attachment_path, all_files, quotes, self.request) + + if kw['template_itemlist'] == '1': + self.request.write('Copy the following listitems into the script. Replace alias with the label you want. Afterwards disable template_itemlist by setting it to 0:
') + for attfile in full : + self.request.write(' * [%(attfile)s %(date)s]
' % { + 'attfile' : attfile, + 'date' : 'alias' + }) + + + i = 0 + z = 1 + cols = int(kw['columns']) + + + n = len(full) + if kw['album'] == '0' : + self.request.write("" % self.kw['border_thick']) + if kw['mode'] == '1' or cols > 1 : + self.request.write('') + self.request.write('') + if z < n : + self.request.write('') + self.request.write('') + if i < n-1 : + self.request.write('') + self.request.write('') + self.request.write('') + + + self.request.write('
') + + + if kw['album'] == '1' : + if kw['front_image'] == '' : + front_image = full[0] + else: + front_image = kw['front_image'] + ii = 0 + for tst in full : + if tst == front_image : + break + ii += 1 + + + for attfile in full : + if kw['album'] == '1' : + if tst == front_image : + i = ii + + + this_description=description[i] + this_exif_date=exif_date[i] + this_webnail=web[i] + this_imgtype=imgtype[i] + this_thumbfile=thumb[i] + + + thumbf=os.path.join(attachment_path,this_thumbfile) + webf=os.path.join(attachment_path,this_webnail) + + + if kw['renew'] == '1': + if os.path.exists(thumbf): + os.unlink(thumbf) + if os.path.exists(webf): + os.unlink(webf) + + if not os.path.exists(webf) or not os.path.exists(thumbf): + infile=os.path.join(attachment_path,attfile) + im = Image.open(infile) + if not os.path.exists(webf): + im.thumbnail(((int(kw['webnail_width'])),((int(kw['webnail_width'])))), Image.ANTIALIAS) + im.save(webf, this_imgtype) + if not os.path.exists(thumbf): + im.thumbnail(((int(kw['thumbnail_width'])),((int(kw['thumbnail_width'])))), + Image.ANTIALIAS) + im.save(thumbf, this_imgtype) + + + if kw['mode'] == '1': + text=mode1_html(current_pagename, + kw['border_thick'], + kw['width'], + kw['thumbnail_width'], + kw['text_width'], + attfile + "," + ','.join(full), + attfile, + to_htmltext(this_description + "!,!" + '!,!'.join(description)), + to_wikiname(self.request,formatter,this_description), + to_htmltext(this_exif_date + "," + ','.join(exif_date)), + this_exif_date, + this_webnail + "," + ','.join(web), + this_webnail, + AttachFile.getAttachUrl(current_pagename, this_thumbfile, self.request), + kw['show_tools'], + kw['show_date'], + kw['show_text'], + self.request + ) + self.request.write(''.join(text)) + + if kw['mode'] == '2': + text=mode2_html(current_pagename, + kw['border_thick'], + kw['width'], + kw['thumbnail_width'], + kw['text_width'], + attfile + "," + ','.join(full), + attfile, + to_htmltext(this_description + "!,!" + '!,!'.join(description)), + to_wikiname(self.request,formatter,this_description), + to_htmltext(this_exif_date + "," + ','.join(exif_date)), + this_exif_date, + this_webnail + "," + ','.join(web), + this_webnail, + AttachFile.getAttachUrl(current_pagename, this_thumbfile, self.request), + kw['show_tools'], + kw['show_date'], + kw['show_text'], + self.request + ) + + if cols > 1 : self.request.write('') + self.request.write(''.join(text)) + if cols > 1 : self.request.write('
') + + if kw['mode'] == '1' or cols > 1: + if kw['album'] == '0' : + if z < cols: + self.request.write('
') + else: + self.request.write('
') + + i += 1 + z += 1 + if z > cols : + z = 1 + + if kw['album'] == '1' : + self.request.write("%(n)s images (%(album_name)s)" % {"n": str(n), "album_name":kw['album_name']}) + break + if kw['album'] == '0' : + if i < n : + self.request.write('
') + + ############################################ + ##TODO: syntax change to formatter - later # + ############################################ + + + + + diff --git a/wiki/parser/Portail.py b/wiki/parser/Portail.py new file mode 100644 index 00000000..961a5a9e --- /dev/null +++ b/wiki/parser/Portail.py @@ -0,0 +1,117 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - Portail parser + + PURPOSE: + Pour afficher un portail a la wikipedia. + + CALLING SEQUENCE: + {{{ + #!Portail + image1.png | title1 | description 1 + image2.png | title2 | description 2 + image3.png | title3 | description 3 + }}} + + CREDIT + Le code est derive de celui de Gallery2. + +""" +from MoinMoin.parser import wiki +import os,string,re,StringIO +from MoinMoin.action import AttachFile + +##################################################################### +# Fonctions # +##################################################################### +# to_wikiname : transfome la chaine text avec le parser wiki +# classique et le formateur formatter +# (je sais pas a quoi sert request, mais faut pas +# l'enlever !) +####### + +def to_wikiname(request,formatter,text): + ##taken from MiniPage + out=StringIO.StringIO() + request.redirect(out) + wikiizer = wiki.Parser(text.strip(),request) + wikiizer.format(formatter) + result=out.getvalue() + request.redirect() + del out + + return result.strip() + +##################################################################### +# PortailFormatter : creer le code html du portail a l'aide de +# de formatter et l'envoie a request +####### +class PortailFormatter: + + def __init__(self, request, formatter): + self.formatter = formatter + self.request = request + self.counter = 0 + + def open(self): + self.request.write(self.formatter.table(1,{'style':'width:100%;padding:10px;'})) + + def cell(self, title, description, image): + if self.counter==0: + self.request.write(self.formatter.table_row(1)) + self.request.write(self.formatter.table_cell(1,{'style':'width:50%;border:none;padding:20px 20px 20px 50px;background:transparent url(\'' + image.replace("\'","\\\'") + '\') center left no-repeat; vertical-align:center;'})) + self.request.write(title) + self.request.write(description) + self.request.write(self.formatter.table_cell(0)) + if self.counter==1: + self.request.write(self.formatter.table_row(0)) + self.counter = (self.counter + 1)%2 + + def close(self): + if self.counter==1: + self.request.write(self.formatter.table_cell(1,{'style':'width:50%;border:none;padding:20px 20px 20px 50px;'})) + self.request.write(self.formatter.table_cell(0)) + self.request.write(self.formatter.table_row(0)) + self.request.write(self.formatter.table(0)) + +##################################################################### +# Parser : classe principale, c'est elle qui parse +####### +class Parser: + + def __init__(self, raw, request, **kw): + self.request = request + self.raw = raw + return + + + def format(self, formatter): + + # on divise le textes en lignes (1 ligne = une entree dans le portail) + quotes = self.raw.split('\n') + + # on récupere le chemin des fichiers attaches pour les images + current_pagename=formatter.page.page_name + attachment_path = AttachFile.getAttachDir(self.request, current_pagename, create=1) + + # on initialise la classe qui va fabriquer le code html + portail = PortailFormatter(self.request, formatter) + portail.open() + + # on traite les ligne une à une + for line in quotes: + line=line.strip() + items=line.split('|',2) + if items.__len__()<3: + self.request.write('') + else: + description=items.pop().strip() + link=items.pop().strip() + image = AttachFile.getAttachUrl(current_pagename, items.pop().strip(), self.request) + + link = to_wikiname(self.request, formatter, link) + description = to_wikiname(self.request, formatter, description) + portail.cell(link, description, image) + portail.close() + + return diff --git a/wiki/parser/__init__.py b/wiki/parser/__init__.py new file mode 100644 index 00000000..e4ed3b60 --- /dev/null +++ b/wiki/parser/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: iso-8859-1 -*- + +from MoinMoin.util import pysupport + +modules = pysupport.getPackageModules(__file__) diff --git a/wiki/parser/latex.py b/wiki/parser/latex.py new file mode 100644 index 00000000..d2f43a75 --- /dev/null +++ b/wiki/parser/latex.py @@ -0,0 +1,214 @@ +#FORMAT python +#!/usr/bin/python +# -*- coding: iso-8859-15 -*- + +""" +New latex formatter using dvipng and tempfile + +Author: JohannesBerg + +This parser (and the corresponding macro) was tested with Python 2.3.4 and + * Debian Linux with out-of-the-box tetex-bin and dvipng packages installed + * Windows XP (not by me) + +In the parser, you can add stuff to the prologue by writing +%%end-prologue%% +somewhere in the document, before that write stuff like \\usepackage and after it put +the actual latex display code. +""" + +Dependencies = [] + +import sha, os, tempfile, shutil, re +from MoinMoin.action import AttachFile +from MoinMoin.Page import Page + +latex_template = r''' +\documentclass[12pt]{article} +\pagestyle{empty} +%(prologue)s +\begin{document} +%(raw)s +\end{document} +''' + +max_pages = 10 +MAX_RUN_TIME = 5 # seconds + +latex = "latex" # edit full path here, e.g. reslimit = "C:\\path\\to\\latex.exe" +dvipng = "dvipng" # edit full path here (or reslimit = r'C:\path\to\latex.exe') + +# last arg must have %s in it! +latex_args = ("--interaction=nonstopmode", "%s.tex") + +# last arg must have %s in it! +dvipng_args = ("-bgTransparent", "-Ttight", "--noghostscript", "-l%s" % max_pages, "%s.dvi") + +# this is formatted with hexdigest(texcode), +# page number and extension are appended by +# the tools +latex_name_template = "latex_%s_p" + +# keep this up-to-date, also with max_pages!! +latex_attachment = re.compile((latex_name_template+'%s%s') % (r'[0-9a-fA-F]{40}', r'[0-9]{1,2}', r'\.png')) + +anchor = re.compile(r'^%%anchor:[ ]*([a-zA-Z0-9_-]+)$', re.MULTILINE | re.IGNORECASE) +# the anchor re must start with a % sign to be ignored by latex as a comment! +end_prologue = '%%end-prologue%%' + +def call_command_in_dir_NT(app, args, targetdir): + reslimit = "runlimit.exe" # edit full path here + os.environ['openin_any'] = 'p' + os.environ['openout_any'] = 'p' + os.environ['shell_escape'] = 'f' + stdouterr = os.popen('%s %d "%s" %s %s < NUL' % (reslimit, MAX_RUN_TIME, targetdir, app, ' '.join(args)), 'r') + output = ''.join(stdouterr.readlines()) + err = stdouterr.close() + if not err is None: + return ' error! exitcode was %d, transscript follows:\n\n%s' % (err,output) + return None + +def call_command_in_dir_unix(app, args, targetdir): + # this is the unix implementation + (r,w) = os.pipe() + pid = os.fork() + if pid == -1: + return 'could not fork' + if pid == 0: + # child + os.close(r) + os.dup2(os.open("/dev/null", os.O_WRONLY), 0) + os.dup2(w, 1) + os.dup2(w, 2) + os.chdir(targetdir) + os.environ['openin_any'] = 'p' + os.environ['openout_any'] = 'p' + os.environ['shell_escape'] = 'f' + import resource + resource.setrlimit(resource.RLIMIT_CPU, + (MAX_RUN_TIME * 1000, MAX_RUN_TIME * 1000)) # docs say this is seconds, but it is msecs on my system. + os.execvp(app, [app] + list(args)) + print "failed to exec()",app + os._exit(2) + else: + # parent + os.close(w) + r = os.fdopen(r,"r") + output = ''.join(r.readlines()) + (npid, exi) = os.waitpid(pid, 0) + r.close() + sig = exi & 0xFF + stat = exi >> 8 + if stat != 0 or sig != 0: + return ' error! exitcode was %d (signal %d), transscript follows:\n\n%s' % (stat,sig,output) + return None + # notreached + +if os.name == 'nt': + call_command_in_dir = call_command_in_dir_NT +else: + call_command_in_dir = call_command_in_dir_unix + + +class Parser: + extensions = ['.tex'] + def __init__ (self, raw, request, **kw): + self.raw = raw + if len(self.raw)>0 and self.raw[0] == '#': + self.raw[0] = '%' + self.request = request + self.exclude = [] + if not hasattr(request, "latex_cleanup_done"): + request.latex_cleanup_done = {} + + def cleanup(self, pagename): + attachdir = AttachFile.getAttachDir(self.request, pagename, create=1) + for f in os.listdir(attachdir): + if not latex_attachment.match(f) is None: + os.remove("%s/%s" % (attachdir, f)) + + def _internal_format(self, formatter, text): + tmp = text.split(end_prologue, 1) + if len(tmp) == 2: + prologue,tex=tmp + else: + prologue = '' + tex = tmp[0] + if callable(getattr(formatter, 'johill_sidecall_emit_latex', None)): + return formatter.johill_sidecall_emit_latex(tex) + return self.get(formatter, tex, prologue, True) + + def format(self, formatter): + self.request.write(self._internal_format(formatter, self.raw)) + + def get(self, formatter, inputtex, prologue, para=False): + if not self.request.latex_cleanup_done.has_key(formatter.page.page_name): + self.request.latex_cleanup_done[formatter.page.page_name] = True + self.cleanup(formatter.page.page_name) + + if len(inputtex) == 0: return '' + + if callable(getattr(formatter, 'johill_sidecall_emit_latex', None)): + return formatter.johill_sidecall_emit_latex(inputtex) + + extra_preamble = '' + preamble_page = self.request.pragma.get('latex_preamble', None) + if preamble_page is not None: + extra_preamble = Page(self.request, preamble_page).get_raw_body() + extra_preamble = re.sub(re.compile('^#'), '%', extra_preamble) + + tex = latex_template % { 'raw': inputtex, 'prologue': extra_preamble + prologue } + enctex = tex.encode('utf-8') + fn = latex_name_template % sha.new(enctex).hexdigest() + + attachdir = AttachFile.getAttachDir(self.request, formatter.page.page_name, create=1) + dst = "%s/%s%%d.png" % (attachdir, fn) + if not os.access(dst % 1, os.R_OK): + tmpdir = tempfile.mkdtemp() + try: + data = open("%s/%s.tex" % (tmpdir, fn), "w") + data.write(enctex) + data.close() + args = list(latex_args) + args[-1] = args[-1] % fn + res = call_command_in_dir(latex, args, tmpdir) + if not res is None: + return formatter.preformatted(1)+formatter.text('latex'+res)+formatter.preformatted(0) + args = list(dvipng_args) + args[-1] = args[-1] % fn + res = call_command_in_dir(dvipng, args, tmpdir) + if not res is None: + return formatter.preformatted(1)+formatter.text('dvipng'+res)+formatter.preformatted(0) + + page = 1 + while os.access("%s/%s%d.png" % (tmpdir, fn, page), os.R_OK): + shutil.copyfile ("%s/%s%d.png" % (tmpdir, fn, page), dst % page) + page += 1 + + finally: + for root,dirs,files in os.walk(tmpdir, topdown=False): + for name in files: + os.remove(os.path.join(root,name)) + for name in dirs: + os.rmdir(os.path.join(root,name)) + os.rmdir(tmpdir) + + result = "" + page = 1 + loop = False + for match in anchor.finditer(inputtex): + result += formatter.anchordef(match.group(1)) + for match in anchor.finditer(prologue): + result += formatter.anchordef(match.group(1)) + while os.access(dst % page, os.R_OK): + url = AttachFile.getAttachUrl(formatter.page.page_name, fn+"%d.png" % page, self.request) + if loop: + result += formatter.linebreak(0)+formatter.linebreak(0) + if para: + result += formatter.paragraph(1) + result += formatter.image(src="%s" % url, alt=inputtex, title=inputtex, align="absmiddle") + if para: + result += formatter.paragraph(0) + page += 1 + loop = True + return result diff --git a/wiki/theme/__init__.py b/wiki/theme/__init__.py new file mode 100644 index 00000000..e4ed3b60 --- /dev/null +++ b/wiki/theme/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: iso-8859-1 -*- + +from MoinMoin.util import pysupport + +modules = pysupport.getPackageModules(__file__) diff --git a/wiki/theme/bde.py b/wiki/theme/bde.py new file mode 100644 index 00000000..dcb61712 --- /dev/null +++ b/wiki/theme/bde.py @@ -0,0 +1,35 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin monobook theme. Uses the css sheet from + http://wikipedia.org, adapting the moin output to fit it. + + Adapted by Jim Clark + Adapted for CR@NS by Nicolas Salles + @license: GNU GPL, see COPYING for details. +""" + +from crans import ThemeCrans + +class Theme(ThemeCrans): + + + # Standard set of style sheets + stylesheets = ( + # media basename + ('all', 'common'), + ('screen', 'crans'), + ('screen', 'bde'), + ('print', 'print'), + ('projection', 'projection'), + ) + +def execute(request): + """ + Generate and return a theme object + + @param request: the request object + @rtype: MoinTheme + @return: Theme object + """ + return Theme(request) + diff --git a/wiki/theme/blackliste.py b/wiki/theme/blackliste.py new file mode 100644 index 00000000..9fcbc4cf --- /dev/null +++ b/wiki/theme/blackliste.py @@ -0,0 +1,467 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin monobook theme. Uses the css sheet from + http://wikipedia.org, adapting the moin output to fit it. + + Adapted by Jim Clark + Adapted for CR@NS by Nicolas Salles + @license: GNU GPL, see COPYING for details. +""" + +from MoinMoin.theme import ThemeBase +from MoinMoin import wikiutil, i18n +from MoinMoin.Page import Page + +class Theme(ThemeBase): + + name = "blackliste" + + # fake _ function to get gettext recognize those texts: + _ = lambda x: x + + icons = { + # key alt icon filename w h + # ------------------------------------------------------------------ + # navibar + 'help': ("%(page_help_contents)s", "moin-help.png", 12, 11), + 'find': ("%(page_find_page)s", "moin-search.png", 12, 12), + 'diff': (_("Diffs"), "moin-diff.png", 22, 22), + 'info': (_("Info"), "moin-info.png", 22, 22), + 'edit': (_("Edit"), "moin-edit.png", 22, 22), + 'unsubscribe':(_("Unsubscribe"), "moin-unsubscribe.png", 22, 22), + 'subscribe': (_("Subscribe"), "moin-subscribe.png",22, 22), + 'raw': (_("Raw"), "moin-raw.png", 12, 13), + 'xml': (_("XML"), "moin-xml.png", 20, 13), + 'print': (_("Print"), "moin-print.png", 16, 14), + 'view': (_("View"), "moin-show.png", 22, 22), + 'home': (_("Home"), "moin-home.png", 13, 12), + 'up': (_("Up"), "moin-parent.png", 15, 13), + # FileAttach + 'attach': ("%(attach_count)s", "moin-attach.png", 7, 15), + # RecentChanges + 'rss': (_("[RSS]"), "moin-rss.png", 36, 14), + 'deleted': (_("[DELETED]"), "moin-deleted.png",60, 12), + 'updated': (_("[UPDATED]"), "moin-updated.png",60, 12), + 'new': (_("[NEW]"), "moin-new.png", 31, 12), + 'diffrc': (_("[DIFF]"), "moin-diff.png", 22, 22), + # General + 'bottom': (_("[BOTTOM]"), "moin-bottom.png", 14, 10), + 'top': (_("[TOP]"), "moin-top.png", 14, 10), + 'www': ("[WWW]", "moin-www.png", 11, 11), + 'mailto': ("[MAILTO]", "moin-email.png", 14, 10), + 'news': ("[NEWS]", "moin-news.png", 10, 11), + 'telnet': ("[TELNET]", "moin-telnet.png", 10, 11), + 'ftp': ("[FTP]", "moin-ftp.png", 11, 11), + 'file': ("[FILE]", "moin-ftp.png", 11, 11), + # search forms + 'searchbutton': ("[?]", "moin-search.png", 12, 12), + 'interwiki': ("[%(wikitag)s]", "moin-inter.png", 16, 16), + } + del _ + + # Standard set of style sheets + stylesheets = ( + # media basename + ('all', 'common'), + ('screen', 'blackliste'), + ('print', 'print'), + ('projection', 'projection'), + ) + + +# Public functions ##################################################### + + def header(self, d, **kw): + """ Assemble wiki header + Here we don't add any menu bars, search bars, etc - instead wait + until the footer. This keeps the HTML cleaner and more accessible, + making sure the main content arrives first. + """ + html = [ + u'
', + u'
', + self.startPage(), + self.msg(d), + self.title(d), + ] + return u'\n'.join(html) + + def footer(self, d, **keywords): + """ Assemble wiki footer + """ + html = [ + # End of page + u'
', + self.endPage(), + u'
', + self.columnone(d), + u'
' + ] + return u'\n'.join(html) + + def columnone(self, d): + """ assemble all the navigation aids for the page + """ + page = d['page'] + html = [ + u'
', + self.editbar(d), + self.username(d), + u'', + self.navibar(d), + self.searchform(d), + self.actionmenu(d), + u'
', + u'', + u'
' + ] + return u'\n'.join(html) + + def headscript(self, d): + """ Override to not output search/action menu javascript. + (perhaps not a good idea) + """ + return '' + + def extendedAttrs(self, title, accesskey): + """ Helper function for assembling titled access key links + """ + return 'title="%(title)s [alt-%(accesskey)s]" accesskey=%(accesskey)s' % \ + {'accesskey' : accesskey, + 'title' : title} + + def editbar(self, d): + """ Display a list of actions for the page. This list will be turned + into a set of tabbed views on the page by the css. + """ + page = d['page'] + if not self.shouldShowEditbar(page): + return '' + + # Use cached editbar if possible. + cacheKey = 'editbar' + cached = self._cache.get(cacheKey) + if cached: + return cached + + request = self.request + _ = self.request.getText + link = wikiutil.link_tag + quotedname = wikiutil.quoteWikinameURL(page.page_name) + # action, title, description, accesskey + tabs = [('view', 'Article', 'View the page content', 'c'), + ('edit', 'Edit', 'Edit this page', 'e'), + ('diff', 'Show Changes', 'Last page modification', 'd'), + ('info', 'Get Info', 'Page history and information', 'h'), + ('subscribe', 'Subscribe', 'Subscribe to updates to this page', 'w')] + + items = [] + current = self.request.form.get('action', ['view'])[0] + for action, title, description, accesskey in tabs: + if action == current: + cls = 'selected' + else: + cls = 'none' + + if action == "subscribe": + items.append(u'
  • %s
  • \n' % (cls, action, self.make_iconlink( + ["subscribe", "unsubscribe"][self.request.user.isSubscribedTo([d['page_name']])], d))) + else: + items.append(u'
  • %s
  • \n' % (cls, action, self.make_iconlink(action, d))) + + html = [ + u'
    ', + u'
      ', + ''.join(items), + u'
    ', + u'
    ' + ] + html = ''.join(html) + # cache for next call + self._cache[cacheKey] = html + + return html + + + def actionmenu(self, d): + """ different implementation of the actionmenu (aka toolbox) + """ + + page = d['page'] + + # Use cached actionmenu if possible. + cacheKey = 'actionmenu' + cached = self._cache.get(cacheKey) + if cached: + return cached + + request = self.request + _ = request.getText + quotedname = wikiutil.quoteWikinameURL(page.page_name) + + menu = [ + 'raw', + 'print', + 'refresh', + 'AttachFile', + 'SpellCheck', + 'LikePages', + 'LocalSiteMap', + 'RenamePage', + 'DeletePage', + ] + + titles = { + 'raw': _('Show Raw Text', formatted=False), + 'print': _('Show Print View', formatted=False), + 'refresh': _('Delete Cache', formatted=False), + 'AttachFile': _('Attach File', formatted=False), + 'SpellCheck': _('Check Spelling', formatted=False), # rename action! + 'RenamePage': _('Rename Page', formatted=False), + 'DeletePage': _('Delete Page', formatted=False), + 'LikePages': _('Show Like Pages', formatted=False), + 'LocalSiteMap': _('Show Local Site Map', formatted=False), + } + + links = [] + + # Format standard actions + available = request.getAvailableActions(page) + for action in menu: + # Enable delete cache only if page can use caching + if action == 'refresh': + if not page.canUseCache(): + break + # Actions which are not available for this wiki, user or page + if action[0].isupper() and not action in available: + break; + + link = wikiutil.link_tag(self.request, \ + quotedname + '?action=' + action, titles[action]) + links.append(link) + + # Add custom actions not in the standard menu + more = [item for item in available if not item in titles] + more.sort() + if more: + # Add more actions (all enabled) + for action in more: + data = {'action': action, 'disabled': ''} + title = Page(request, action).split_title(request, force=1) + # Use translated version if available + title = _(title, formatted=False) + link = wikiutil.link_tag(self.request, \ + quotedname + '?action=' + action, title) + links.append(link) + + html = [ + u'
    ', + u'
    Toolbox
    ', + u'
    ', + u'
      ', + u'
    • %s
    ' % '\n
  • '.join(links), + u'', + u'
  • ', + u'
    ', + ] + html = ''.join(html) + # cache for next call + self._cache[cacheKey] = html + return html + + + def username(self, d): + """ Assemble the username / userprefs link + Copied from the base class, modified to include hotkeys and link titles + """ + from MoinMoin.Page import Page + request = self.request + _ = request.getText + + userlinks = [] + # Add username/homepage link for registered users. We don't care + # if it exists, the user can create it. + if request.user.valid: + homepage = Page(request, request.user.name) + title = homepage.split_title(request) + attrs = self.extendedAttrs(_('User Page'), '.') + homelink = homepage.link_to(request, text=title, attrs=attrs) + userlinks.append(homelink) + + # Set pref page to localized Preferences page + attrs = self.extendedAttrs(_('My Preferences'), 'u') + prefpage = wikiutil.getSysPage(request, 'UserPreferences') + title = prefpage.split_title(request) + userlinks.append(prefpage.link_to(request, text=title, attrs=attrs)) + + # Add a logout link (not sure this is really necessary + attrs = self.extendedAttrs(_('log out'), 'o') + page = d['page'] + url = wikiutil.quoteWikinameURL(page.page_name) + \ + '?action=userform&logout=1' + link = wikiutil.link_tag(self.request, url, 'log out', attrs=attrs) + userlinks.append(link) + + else: + # Add prefpage links with title: Login + prefpage = wikiutil.getSysPage(request, 'UserPreferences') + attrs = self.extendedAttrs('Logging in is not required, but brings benefits', 'o') + userlinks.append(prefpage.link_to(request, text=_("Login"), attrs=attrs)) + + html = [ + u'
    ', + u'
      ', + u'
    • %s
    ' % '\n
  • '.join(userlinks), + u'', + u'
  • ' + ] + return ''.join(html) + + def searchform(self, d): + """ assemble HTML code for the search form + Tweaks from the bass class to wrap in a 'portlet' class, move the + description to a header tag, add an access key of 'f' and + add SearchButton class for the buttons + """ + _ = self.request.getText + form = self.request.form + updates = { + 'search_label' : _('Search:'), + 'search_value': wikiutil.escape(form.get('value', [''])[0], 1), + 'search_full_label' : _('Text', formatted=False), + 'search_title_label' : _('Titles', formatted=False), + } + d.update(updates) + + html = u''' + +''' % d + return html + + def shouldShowEditbar(self, page): + """ Override to include the editbar on edit/preview pages. + (at the risk that the user may accidentally cancel an edit) + """ + if (page.exists(includeDeleted=1) and + self.request.user.may.read(page.page_name)): + return True + return False + + + def navibar(self, d): + """ Alterations from the base class to include access keys and + descriptions for FrontPage/RecentChanges + """ + request = self.request + found = {} # pages we found. prevent duplicates + links = [] # navibar items + current = d['page_name'] + + # Process config navi_bar + if request.cfg.navi_bar: + for text in request.cfg.navi_bar: + pagename, link = self.splitNavilink(text) + if pagename == d['page_front_page']: + attrs = self.extendedAttrs('Visit the main page', 'z') + elif pagename == 'RecentChanges': + attrs = self.extendedAttrs('List of recent changes in this wiki', 'r') + else: + attrs = '' + + a = wikiutil.link_tag(request, pagename, attrs=attrs) + links.append(a) + found[pagename] = 1 + + # Add user links to wiki links, eliminating duplicates. + userlinks = request.user.getQuickLinks() + for text in userlinks: + # Split text without localization, user know what she wants + pagename, link = self.splitNavilink(text, localize=0) + if not pagename in found: + a = wikiutil.link_tag(request, pagename, attrs=attrs) + links.append(a) + found[pagename] = 1 + + html = [ + u'
    ', + u'
    ', + u'' % '\n
  • '.join(links), + u'', + u'
  • ', + u'
    ', + ] + return ''.join(html) + + def pageinfo(self, page): + """ Simple override from base class. + Remove

    so footer isn't too big + """ + _ = self.request.getText + + if self.shouldShowPageinfo(page): + info = page.lastEditInfo() + if info: + if info['editor']: + info = _("last edited %(time)s by %(editor)s") % info + else: + info = _("last modified %(time)s") % info + return info + return '' + + def rtl_stylesheet(self, d): + """ monobook uses a separate css page for rtl alterations. + Add the rtl stylesheet if the user needs it + """ + link = ('') + html = [] + if i18n.getDirection(self.request.lang) == 'rtl': + prefix = self.cfg.url_prefix + href = '%s/%s/css/%s.css' % (prefix, self.name, 'rtl') + html.append(link % (self.stylesheetsCharset, 'all', href)) + return '\n'.join(html) + + def html_head(self, d): + """ Tweak the sending of the head, to include right-to-left + alterations if necessary + """ + html = [ + u'%(title)s - %(sitename)s' % d, + self.headscript(d), # Should move to separate .js file + self.html_stylesheets(d), + self.rtl_stylesheet(d), + self.rsslink(), + ] + return '\n'.join(html) + +def execute(request): + """ + Generate and return a theme object + + @param request: the request object + @rtype: MoinTheme + @return: Theme object + """ + return Theme(request) diff --git a/wiki/theme/crans-test.py b/wiki/theme/crans-test.py new file mode 100644 index 00000000..987512d7 --- /dev/null +++ b/wiki/theme/crans-test.py @@ -0,0 +1,496 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin monobook theme. Uses the css sheet from + http://wikipedia.org, adapting the moin output to fit it. + + Adapted by Jim Clark + Adapted for CR@NS by Nicolas Salles + @license: GNU GPL, see COPYING for details. +""" + +from MoinMoin.theme import ThemeBase +from MoinMoin import wikiutil, i18n +from MoinMoin.Page import Page + +class ThemeCrans(ThemeBase): + + name = "crans" + + # fake _ function to get gettext recognize those texts: + _ = lambda x: x + + icons = { + # key alt icon filename w h + # ------------------------------------------------------------------ + # navibar + 'help': ("%(page_help_contents)s", "moin-help.png", 12, 11), + 'find': ("%(page_find_page)s", "moin-search.png", 12, 12), + 'diff': (_("Diffs"), "moin-diff.png", 22, 22), + 'info': (_("Info"), "moin-info.png", 22, 22), + 'edit': (_("Edit"), "moin-edit.png", 22, 22), + 'unsubscribe':(_("Unsubscribe"), "moin-unsubscribe.png", 22, 22), + 'subscribe': (_("Subscribe"), "moin-subscribe.png",22, 22), + 'raw': (_("Raw"), "moin-raw.png", 12, 13), + 'xml': (_("XML"), "moin-xml.png", 20, 13), + 'print': (_("Print"), "moin-print.png", 16, 14), + 'view': (_("View"), "moin-show.png", 22, 22), + 'home': (_("Home"), "moin-home.png", 13, 12), + 'up': (_("Up"), "moin-parent.png", 15, 13), + # FileAttach + 'attach': ("%(attach_count)s", "moin-attach.png", 7, 15), + # RecentChanges + 'rss': (_("[RSS]"), "moin-rss.png", 36, 14), + 'deleted': (_("[DELETED]"), "moin-deleted.png",60, 12), + 'updated': (_("[UPDATED]"), "moin-updated.png",60, 12), + 'new': (_("[NEW]"), "moin-new.png", 31, 12), + 'diffrc': (_("[DIFF]"), "moin-diff.png", 22, 22), + # General + 'bottom': (_("[BOTTOM]"), "moin-bottom.png", 14, 10), + 'top': (_("[TOP]"), "moin-top.png", 14, 10), + 'www': ("[WWW]", "moin-www.png", 11, 11), + 'mailto': ("[MAILTO]", "moin-email.png", 14, 10), + 'news': ("[NEWS]", "moin-news.png", 10, 11), + 'telnet': ("[TELNET]", "moin-telnet.png", 10, 11), + 'ftp': ("[FTP]", "moin-ftp.png", 11, 11), + 'file': ("[FILE]", "moin-ftp.png", 11, 11), + # search forms + 'searchbutton': ("[?]", "moin-search.png", 12, 12), + 'interwiki': ("[%(wikitag)s]", "moin-inter.png", 16, 16), + } + del _ + + # Standard set of style sheets + stylesheets = ( + # media basename + ('all', 'common'), + ('screen', 'crans'), + ('print', 'print'), + ('projection', 'projection'), + ) + + +# Public functions ##################################################### + + def header(self, d, **kw): + """ Assemble wiki header + Here we don't add any menu bars, search bars, etc - instead wait + until the footer. This keeps the HTML cleaner and more accessible, + making sure the main content arrives first. + """ + html = [ + u'

    ', + u'
    ', + self.startPage(), + self.msg(d), + self.title(d), + ] + return u'\n'.join(html) + + def footer(self, d, **keywords): + """ Assemble wiki footer + """ + html = [ + # End of page + u'
    ', + self.endPage(), + u'
    ', + self.columnone(d), + u'
    ' + ] + return u'\n'.join(html) + + def columnone(self, d): + """ assemble all the navigation aids for the page + """ + page = d['page'] + html = [ + u'
    ', + self.editbar(d), + self.username(d), + u'', + self.navibar(d), + self.searchform(d), + self.actionmenu(d), + u'
    ', + u'', + u'
    ' + ] + return u'\n'.join(html) + + def headscript(self, d): + """ Override to not output search/action menu javascript. + (perhaps not a good idea) + """ + return '' + + def extendedAttrs(self, title, accesskey): + """ Helper function for assembling titled access key links + """ + return 'title="%(title)s [alt-%(accesskey)s]" accesskey=%(accesskey)s' % \ + {'accesskey' : accesskey, + 'title' : title} + + def editbar(self, d): + """ Display a list of actions for the page. This list will be turned + into a set of tabbed views on the page by the css. + """ + page = d['page'] + if not self.shouldShowEditbar(page): + return '' + + # Use cached editbar if possible. + cacheKey = 'editbar' + cached = self._cache.get(cacheKey) + if cached: + return cached + + request = self.request + _ = self.request.getText + link = wikiutil.link_tag + quotedname = wikiutil.quoteWikinameURL(page.page_name) + # action, title, description, accesskey + tabs = [('view', 'Article', 'View the page content', 'c'), + ('edit', 'Edit', 'Edit this page', 'e'), + ('diff', 'Show Changes', 'Last page modification', 'd'), + ('info', 'Get Info', 'Page history and information', 'h'), + ('subscribe', 'Subscribe', 'Subscribe to updates to this page', 'w')] + + items = [] + current = self.request.form.get('action', ['view'])[0] + for action, title, description, accesskey in tabs: + if action == current: + cls = 'selected' + else: + cls = 'none' + + if action == "subscribe": + items.append(u'
  • %s
  • \n' % (cls, action, self.make_iconlink( + ["subscribe", "unsubscribe"][self.request.user.isSubscribedTo([d['page_name']])], d))) + else: + items.append(u'
  • %s
  • \n' % (cls, action, self.make_iconlink(action, d))) + + html = [ + u'
    ', + u'
      ', + ''.join(items), + u'
    ', + u'
    ' + ] + html = ''.join(html) + # cache for next call + self._cache[cacheKey] = html + + return html + + + def actionmenu(self, d): + """ different implementation of the actionmenu (aka toolbox) + """ + + page = d['page'] + + # Use cached actionmenu if possible. + cacheKey = 'actionmenu' + cached = self._cache.get(cacheKey) + if cached: + return cached + + request = self.request + _ = request.getText + quotedname = wikiutil.quoteWikinameURL(page.page_name) + + menu = [ + 'raw', + 'print', + 'refresh', + 'AttachFile', + 'SpellCheck', + 'LikePages', + 'LocalSiteMap', + 'RenamePage', + 'DeletePage', + ] + + titles = { + 'raw': _('Show Raw Text', formatted=False), + 'print': _('Show Print View', formatted=False), + 'refresh': _('Delete Cache', formatted=False), + 'AttachFile': _('Attach File', formatted=False), + 'SpellCheck': _('Check Spelling', formatted=False), # rename action! + 'RenamePage': _('Rename Page', formatted=False), + 'DeletePage': _('Delete Page', formatted=False), + 'LikePages': _('Show Like Pages', formatted=False), + 'LocalSiteMap': _('Show Local Site Map', formatted=False), + } + + links = [] + + # Format standard actions + available = request.getAvailableActions(page) + for action in menu: + # Enable delete cache only if page can use caching + if action == 'refresh': + if not page.canUseCache(): + break + # Actions which are not available for this wiki, user or page + if action[0].isupper() and not action in available: + break; + + link = wikiutil.link_tag(self.request, \ + quotedname + '?action=' + action, titles[action]) + links.append(link) + + # Add custom actions not in the standard menu + more = [item for item in available if not item in titles] + more.sort() + if more: + # Add more actions (all enabled) + for action in more: + data = {'action': action, 'disabled': ''} + title = Page(request, action).split_title(request, force=1) + # Use translated version if available + title = _(title, formatted=False) + link = wikiutil.link_tag(self.request, \ + quotedname + '?action=' + action, title) + links.append(link) + + html = [ + u'
    ', + u'
    Toolbox
    ', + u'
    ', + u'
      ', + u'
    • %s
    ' % '\n
  • '.join(links), + u'', + u'
  • ', + u'
    ', + ] + html = ''.join(html) + # cache for next call + self._cache[cacheKey] = html + return html + + + def username(self, d): + """ Assemble the username / userprefs link + Copied from the base class, modified to include hotkeys and link titles + """ + from MoinMoin.Page import Page + request = self.request + _ = request.getText + + userlinks = [] + # Add username/homepage link for registered users. We don't care + # if it exists, the user can create it. + if request.user.valid: + homepage = Page(request, request.user.name) + title = homepage.split_title(request) + attrs = self.extendedAttrs(_('User Page'), '.') + homelink = homepage.link_to(request, text=title, attrs=attrs) + userlinks.append(homelink) + + # Set pref page to localized Preferences page + attrs = self.extendedAttrs(_('My Preferences'), 'u') + prefpage = wikiutil.getSysPage(request, 'UserPreferences') + title = prefpage.split_title(request) + userlinks.append(prefpage.link_to(request, text=title, attrs=attrs)) + + # Add a logout link (not sure this is really necessary + attrs = self.extendedAttrs(_('log out'), 'o') + page = d['page'] + url = wikiutil.quoteWikinameURL(page.page_name) + \ + '?action=userform&logout=1' + link = wikiutil.link_tag(self.request, url, 'log out', attrs=attrs) + userlinks.append(link) + + else: + # Add prefpage links with title: Login + prefpage = wikiutil.getSysPage(request, 'UserPreferences') + attrs = self.extendedAttrs('Logging in is not required, but brings benefits', 'o') + userlinks.append(prefpage.link_to(request, text=_("Login"), attrs=attrs)) + + html = [ + u'
    ', + u'
      ', + u'
    • %s
    ' % '\n
  • '.join(userlinks), + u'', + u'
  • ' + ] + return ''.join(html) + + def searchform(self, d): + """ assemble HTML code for the search form + Tweaks from the bass class to wrap in a 'portlet' class, move the + description to a header tag, add an access key of 'f' and + add SearchButton class for the buttons + """ + _ = self.request.getText + form = self.request.form + updates = { + 'search_label' : _('Search:'), + 'search_value': wikiutil.escape(form.get('value', [''])[0], 1), + 'search_full_label' : _('Text', formatted=False), + 'search_title_label' : _('Titles', formatted=False), + } + d.update(updates) + + html = u''' + +''' % d + return html + + def shouldShowEditbar(self, page): + """ Override to include the editbar on edit/preview pages. + (at the risk that the user may accidentally cancel an edit) + """ + if (page.exists(includeDeleted=1) and + self.request.user.may.read(page.page_name)): + return True + return False + + + def navibar(self, d): + """ Alterations from the base class to include access keys and + descriptions for FrontPage/RecentChanges + """ + request = self.request + found = {} # pages we found. prevent duplicates + links = [] # navibar items + current = d['page_name'] + + # Process config navi_bar + if request.cfg.navi_bar: + for text in request.cfg.navi_bar: + pagename, link = self.splitNavilink(text) + if pagename == d['page_front_page']: + attrs = self.extendedAttrs('Visit the main page', 'z') + elif pagename == 'RecentChanges': + attrs = self.extendedAttrs('List of recent changes in this wiki', 'r') + else: + attrs = '' + + a = wikiutil.link_tag(request, pagename, attrs=attrs) + links.append(a) + found[pagename] = 1 + + # Add user links to wiki links, eliminating duplicates. + userlinks = request.user.getQuickLinks() + for text in userlinks: + # Split text without localization, user know what she wants + pagename, link = self.splitNavilink(text, localize=0) + if not pagename in found: + a = wikiutil.link_tag(request, pagename, attrs=attrs) + links.append(a) + found[pagename] = 1 + + html = [ + u'
    ', + u'
    ', + u'' % '\n
  • '.join(links), + u'', + u'
  • ', + u'
    ', + ] + return ''.join(html) + + def pageinfo(self, page): + """ Simple override from base class. + Remove

    so footer isn't too big + """ + _ = self.request.getText + + if self.shouldShowPageinfo(page): + info = page.lastEditInfo() + if info: + if info['editor']: + info = _("last edited %(time)s by %(editor)s") % info + else: + info = _("last modified %(time)s") % info + return info + return '' + + def rtl_stylesheet(self, d): + """ monobook uses a separate css page for rtl alterations. + Add the rtl stylesheet if the user needs it + """ + link = ('') + html = [] + if i18n.getDirection(self.request.lang) == 'rtl': + prefix = self.cfg.url_prefix + href = '%s/%s/css/%s.css' % (prefix, self.name, 'rtl') + html.append(link % (self.stylesheetsCharset, 'all', href)) + return '\n'.join(html) + + def html_head(self, d): + """ Tweak the sending of the head, to include right-to-left + alterations if necessary + """ + html = [ + u'%(title)s - %(sitename)s' % d, + self.headscript(d), # Should move to separate .js file + self.html_stylesheets(d), + self.rtl_stylesheet(d), + self.rsslink(), + ] + return u'\n'.join(html) + + def html_stylesheets(self, d): + """ + On fixe le chemin d'acces vers les feuilles de style + """ + html = [] + for media, nom in self.stylesheets: + html.append(u'' % (media, nom)) + + + +class Theme(ThemeCrans): + + name = "crans" + + # Standard set of style sheets + stylesheets = ( + # media basename + ('all', 'common'), + ('screen', 'crans'), + ('print', 'print'), + ('projection', 'projection'), + ) + + + + + + + +def execute(request): + """ + Generate and return a theme object + + @param request: the request object + @rtype: MoinTheme + @return: Theme object + """ + return Theme(request) diff --git a/wiki/theme/crans.py b/wiki/theme/crans.py new file mode 100644 index 00000000..dd3ab3d7 --- /dev/null +++ b/wiki/theme/crans.py @@ -0,0 +1,502 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin monobook theme. Uses the css sheet from + http://wikipedia.org, adapting the moin output to fit it. + + Adapted by Jim Clark + Adapted for CR@NS by Nicolas Salles + @license: GNU GPL, see COPYING for details. +""" + +from MoinMoin.theme import ThemeBase +from MoinMoin import wikiutil, i18n +from MoinMoin.Page import Page + +class ThemeCrans(ThemeBase): + + name = "crans" + + # fake _ function to get gettext recognize those texts: + _ = lambda x: x + + icons = { + # key alt icon filename w h + # ------------------------------------------------------------------ + # navibar + 'help': ("%(page_help_contents)s", "moin-help.png", 12, 11), + 'find': ("%(page_find_page)s", "moin-search.png", 12, 12), + 'diff': (_("Diffs"), "moin-diff.png", 22, 22), + 'info': (_("Info"), "moin-info.png", 22, 22), + 'edit': (_("Edit"), "moin-edit.png", 22, 22), + 'unsubscribe':(_("Unsubscribe"), "moin-unsubscribe.png", 22, 22), + 'subscribe': (_("Subscribe"), "moin-subscribe.png",22, 22), + 'raw': (_("Raw"), "moin-raw.png", 12, 13), + 'xml': (_("XML"), "moin-xml.png", 20, 13), + 'print': (_("Print"), "moin-print.png", 16, 14), + 'view': (_("View"), "moin-show.png", 22, 22), + 'home': (_("Home"), "moin-home.png", 13, 12), + 'up': (_("Up"), "moin-parent.png", 15, 13), + # format + 'format': (_("[TEX]"), "moin-tex.png", 20, 22), + # FileAttach + 'attach': ("%(attach_count)s", "moin-attach.png", 7, 15), + # RecentChanges + 'rss': (_("[RSS]"), "moin-rss.png", 36, 14), + 'deleted': (_("[DELETED]"), "moin-deleted.png",60, 12), + 'updated': (_("[UPDATED]"), "moin-updated.png",60, 12), + 'new': (_("[NEW]"), "moin-new.png", 31, 12), + 'diffrc': (_("[DIFF]"), "moin-diff.png", 22, 22), + # General + 'bottom': (_("[BOTTOM]"), "moin-bottom.png", 14, 10), + 'top': (_("[TOP]"), "moin-top.png", 14, 10), + 'www': ("[WWW]", "moin-www.png", 11, 11), + 'mailto': ("[MAILTO]", "moin-email.png", 14, 10), + 'news': ("[NEWS]", "moin-news.png", 10, 11), + 'telnet': ("[TELNET]", "moin-telnet.png", 10, 11), + 'ftp': ("[FTP]", "moin-ftp.png", 11, 11), + 'file': ("[FILE]", "moin-ftp.png", 11, 11), + # search forms + 'searchbutton': ("[?]", "moin-search.png", 12, 12), + 'interwiki': ("[%(wikitag)s]", "moin-inter.png", 16, 16), + } + del _ + + # Standard set of style sheets + stylesheets = ( + # media basename + ('all', 'common'), + ('screen', 'crans'), + ('print', 'print'), + ('projection', 'projection'), + ) + + +# Public functions ##################################################### + + def header(self, d, **kw): + """ Assemble wiki header + Here we don't add any menu bars, search bars, etc - instead wait + until the footer. This keeps the HTML cleaner and more accessible, + making sure the main content arrives first. + """ + html = [ + u'

    ', + u'
    ', + self.startPage(), + self.msg(d), + self.title(d), + ] + return u'\n'.join(html) + + def footer(self, d, **keywords): + """ Assemble wiki footer + """ + html = [ + # End of page + u'
    ', + self.endPage(), + u'
    ', + u'
    ', + self.columnone(d), + u'
    ' + ] + return u'\n'.join(html) + + def columnone(self, d): + """ assemble all the navigation aids for the page + """ + page = d['page'] + html = [ + u'
    ', + self.editbar(d), + self.username(d), + u'', + self.navibar(d), + self.searchform(d), + self.actionmenu(d), + u'
    ', + u'', + u'
    ' + ] + return u'\n'.join(html) + + def headscript(self, d): + """ Override to not output search/action menu javascript. + (perhaps not a good idea) + """ + return '' + + def extendedAttrs(self, title, accesskey): + """ Helper function for assembling titled access key links + """ + return 'title="%(title)s [alt-%(accesskey)s]" accesskey=%(accesskey)s' % \ + {'accesskey' : accesskey, + 'title' : title} + + def editbar(self, d): + """ Display a list of actions for the page. This list will be turned + into a set of tabbed views on the page by the css. + """ + page = d['page'] + if not self.shouldShowEditbar(page): + return '' + + # Use cached editbar if possible. + cacheKey = 'editbar' + cached = self._cache.get(cacheKey) + if cached: + return cached + + request = self.request + _ = self.request.getText + link = wikiutil.link_tag + quotedname = wikiutil.quoteWikinameURL(page.page_name) + # action, title, description, accesskey + tabs = [('view', 'Article', 'View the page content', 'c'), + ('edit', 'Edit', 'Edit this page', 'e'), + ('diff', 'Show Changes', 'Last page modification', 'd'), + ('info', 'Get Info', 'Page history and information', 'h'), + #('format', 'Latex', 'Source Latex', 'l'), + ('subscribe', 'Subscribe', 'Subscribe to updates to this page', 'w')] + + items = [] + current = self.request.form.get('action', ['view'])[0] + for action, title, description, accesskey in tabs: + if action == current: + cls = 'selected' + else: + cls = 'none' + + if action == "subscribe": + items.append(u'
  • %s
  • \n' % (cls, action, self.make_iconlink( + ["subscribe", "unsubscribe"][self.request.user.isSubscribedTo([d['page_name']])], d))) + else: + items.append(u'
  • %s
  • \n' % (cls, action, self.make_iconlink(action, d))) + + html = [ + u'
    ', + u'
      ', + ''.join(items), + u'
    ', + u'
    ' + ] + html = ''.join(html) + # cache for next call + self._cache[cacheKey] = html + + return html + + + def actionmenu(self, d): + """ different implementation of the actionmenu (aka toolbox) + """ + + page = d['page'] + + # Use cached actionmenu if possible. + cacheKey = 'actionmenu' + cached = self._cache.get(cacheKey) + if cached: + return cached + + request = self.request + _ = request.getText + quotedname = wikiutil.quoteWikinameURL(page.page_name) + + menu = [ + 'raw', + 'print', + u'format&mimetype=text/latex', + 'refresh', + 'AttachFile', + 'SpellCheck', + 'LikePages', + 'LocalSiteMap', + 'RenamePage', + 'DeletePage', + ] + + titles = { + 'raw': _('Show Raw Text', formatted=False), + 'print': _('Show Print View', formatted=False), + u'format&mimetype=text/latex': _('Obtenir le code latex', formatted=False), + 'refresh': _('Delete Cache', formatted=False), + 'AttachFile': _('Attach File', formatted=False), + 'SpellCheck': _('Check Spelling', formatted=False), # rename action! + 'RenamePage': _('Rename Page', formatted=False), + 'DeletePage': _('Delete Page', formatted=False), + 'LikePages': _('Show Like Pages', formatted=False), + 'LocalSiteMap': _('Show Local Site Map', formatted=False), + } + + links = [] + + # Format standard actions + available = request.getAvailableActions(page) + for action in menu: + # Enable delete cache only if page can use caching + if action == 'refresh': + if not page.canUseCache(): + break + # Actions which are not available for this wiki, user or page + if action[0].isupper() and not action in available: + break; + + link = wikiutil.link_tag(self.request, \ + quotedname + '?action=' + action, titles[action]) + links.append(link) + + # Add custom actions not in the standard menu + more = [item for item in available if not item in titles] + more.sort() + if more: + # Add more actions (all enabled) + for action in more: + data = {'action': action, 'disabled': ''} + title = Page(request, action).split_title(request, force=1) + # Use translated version if available + title = _(title, formatted=False) + link = wikiutil.link_tag(self.request, \ + quotedname + '?action=' + action, title) + links.append(link) + + html = [ + u'
    ', + u'
    Toolbox
    ', + u'
    ', + u'
      ', + u'
    • %s
    ' % '\n
  • '.join(links), + u'', + u'
  • ', + u'
    ', + ] + html = ''.join(html) + # cache for next call + self._cache[cacheKey] = html + return html + + + def username(self, d): + """ Assemble the username / userprefs link + Copied from the base class, modified to include hotkeys and link titles + """ + from MoinMoin.Page import Page + request = self.request + _ = request.getText + + userlinks = [] + # Add username/homepage link for registered users. We don't care + # if it exists, the user can create it. + if request.user.valid: + homepage = Page(request, request.user.name) + title = homepage.split_title(request) + attrs = self.extendedAttrs(_('User Page'), '.') + homelink = homepage.link_to(request, text=title, attrs=attrs) + userlinks.append(homelink) + + # Set pref page to localized Preferences page + attrs = self.extendedAttrs(_('My Preferences'), 'u') + prefpage = wikiutil.getSysPage(request, 'UserPreferences') + title = prefpage.split_title(request) + userlinks.append(prefpage.link_to(request, text=title, attrs=attrs)) + + # Add a logout link (not sure this is really necessary + attrs = self.extendedAttrs(_('log out'), 'o') + page = d['page'] + url = wikiutil.quoteWikinameURL(page.page_name) + \ + '?action=userform&logout=1' + link = wikiutil.link_tag(self.request, url, 'log out', attrs=attrs) + userlinks.append(link) + + else: + # Add prefpage links with title: Login + prefpage = wikiutil.getSysPage(request, 'UserPreferences') + attrs = self.extendedAttrs('Logging in is not required, but brings benefits', 'o') + userlinks.append(prefpage.link_to(request, text=_("Login"), attrs=attrs)) + + html = [ + u'
    ', + u'
      ', + u'
    • %s
    ' % '\n
  • '.join(userlinks), + u'', + u'
  • ' + ] + return ''.join(html) + + def searchform(self, d): + """ assemble HTML code for the search form + Tweaks from the bass class to wrap in a 'portlet' class, move the + description to a header tag, add an access key of 'f' and + add SearchButton class for the buttons + """ + _ = self.request.getText + form = self.request.form + updates = { + 'search_label' : _('Search:'), + 'search_value': wikiutil.escape(form.get('value', [''])[0], 1), + 'search_full_label' : _('Text', formatted=False), + 'search_title_label' : _('Titles', formatted=False), + } + d.update(updates) + + html = u''' + +''' % d + return html + + def shouldShowEditbar(self, page): + """ Override to include the editbar on edit/preview pages. + (at the risk that the user may accidentally cancel an edit) + """ + if (page.exists(includeDeleted=1) and + self.request.user.may.read(page.page_name)): + return True + return False + + + def navibar(self, d): + """ Alterations from the base class to include access keys and + descriptions for FrontPage/RecentChanges + """ + request = self.request + found = {} # pages we found. prevent duplicates + links = [] # navibar items + current = d['page_name'] + + # Process config navi_bar + if request.cfg.navi_bar: + for text in request.cfg.navi_bar: + pagename, link = self.splitNavilink(text) + if pagename == d['page_front_page']: + attrs = self.extendedAttrs('Visit the main page', 'z') + elif pagename == 'RecentChanges': + attrs = self.extendedAttrs('List of recent changes in this wiki', 'r') + else: + attrs = '' + + a = wikiutil.link_tag(request, pagename, attrs=attrs) + links.append(a) + found[pagename] = 1 + + # Add user links to wiki links, eliminating duplicates. + userlinks = request.user.getQuickLinks() + for text in userlinks: + # Split text without localization, user know what she wants + pagename, link = self.splitNavilink(text, localize=0) + if not pagename in found: + a = wikiutil.link_tag(request, pagename, attrs=attrs) + links.append(a) + found[pagename] = 1 + + html = [ + u'
    ', + u'
    ', + u'' % '\n
  • '.join(links), + u'', + u'
  • ', + u'
    ', + ] + return ''.join(html) + + def pageinfo(self, page): + """ Simple override from base class. + Remove

    so footer isn't too big + """ + _ = self.request.getText + + if self.shouldShowPageinfo(page): + info = page.lastEditInfo() + if info: + if info['editor']: + info = _("last edited %(time)s by %(editor)s") % info + else: + info = _("last modified %(time)s") % info + return info + return '' + + def rtl_stylesheet(self, d): + """ monobook uses a separate css page for rtl alterations. + Add the rtl stylesheet if the user needs it + """ + link = ('') + html = [] + if i18n.getDirection(self.request.lang) == 'rtl': + prefix = self.cfg.url_prefix + href = '%s/%s/css/%s.css' % (prefix, self.name, 'rtl') + html.append(link % (self.stylesheetsCharset, 'all', href)) + return '\n'.join(html) + + def html_head(self, d): + """ Tweak the sending of the head, to include right-to-left + alterations if necessary + """ + html = [ + u'%(title)s - %(sitename)s' % d, + self.headscript(d), # Should move to separate .js file + self.html_stylesheets(d), + self.rtl_stylesheet(d), + self.rsslink(), + ] + return u' \n'.join(html) + + def html_stylesheets(self, d): + """ + On fixe le chemin d'acces vers les feuilles de style + """ + html = [] + for media, nom in self.stylesheets: + html.append(u'' % (media, nom)) + return u'\n'.join(html) + + +class Theme(ThemeCrans): + + name = "crans" + + # Standard set of style sheets + stylesheets = ( + # media basename + ('all', 'common'), + ('screen', 'crans'), + ('print', 'print'), + ('projection', 'projection'), + ) + + + + + + + +def execute(request): + """ + Generate and return a theme object + + @param request: the request object + @rtype: MoinTheme + @return: Theme object + """ + return Theme(request) diff --git a/wiki/theme/ensanime.py b/wiki/theme/ensanime.py new file mode 100644 index 00000000..29294532 --- /dev/null +++ b/wiki/theme/ensanime.py @@ -0,0 +1,24 @@ +from crans import ThemeCrans + +class Theme(ThemeCrans): + + # Standard set of style sheets + stylesheets = ( + # media basename + ('all', 'common'), + ('all', 'common-reverse'), + ('screen', 'crans'), + ('screen', 'ensanime'), + ('print', 'print'), + ('projection', 'projection'), + ) + +def execute(request): + """ + Generate and return a theme object + + @param request: the request object + @rtype: MoinTheme + @return: Theme object + """ + return Theme(request) diff --git a/wiki/theme/matrix.py b/wiki/theme/matrix.py new file mode 100644 index 00000000..1495ae47 --- /dev/null +++ b/wiki/theme/matrix.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" + MoinMoin technical theme + + @copyright: (c) 2003-2004 by Nir Soffer + @license: GNU GPL, see COPYING for details. +""" + +from MoinMoin.theme import modern + + +class Theme(modern.Theme): + + name = "matrix" + + +def execute(request): + """ Generate and return a theme object + + @param request: the request object + @rtype: Theme instance + @return: Theme object + """ + return Theme(request) + diff --git a/wiki/theme/mentalhealth.py b/wiki/theme/mentalhealth.py new file mode 100644 index 00000000..d87e0e7a --- /dev/null +++ b/wiki/theme/mentalhealth.py @@ -0,0 +1,195 @@ +# -*- coding: iso-8859-1 -*- +""" + Top Matter: + MentalHealth is a MoinMoin theme + by robin.escalation@ACM.org + dated this 20th of April 2005 + + let's call it version 0.9 + and by golly this is GPLed + + Description: + It may look something like the standard "rightsidebar" theme. + But in reality it is so much better. ;-) + + For example: + * smaller type so more fits + * very nice colours to soothe the soul + * but main page still white background so no clashes + * nice alignment, sizes etc. + * search form integrated with right panel look + * got rid of damn ugly action pulldown menu + * no loss of functionality + + Seems to look good but if you have an issue on a browser then + let me know. + + Accompanying files: + No files changed in folder "img". + Only file changed in folder "css" is "screen.css". + (Though see next.) + + Skinning the theme: + This theme uses four compatible colours, as mentioned in the + header "screen.css". If you like you can search'n'replace for + those more to your liking. I have a couple of alternate versions + of the stylesheet, named screen01.css, screen02.css, etc., just + to show how this works. Rename each in tun to screen.css and + check it out. + + Installation: + * move mentalhealth.py to $python/Lib/site-packages/MoinMoin/theme/ + * move the mentalhealth folder under $python/share/moin/htdocs/ + + And Finally: + "Oh we're in love with beauty, + We're in love with wealth, + We're in love with mental health." + -- Julian Cope + +""" + +from MoinMoin.theme import ThemeBase +from MoinMoin.wikiutil import link_tag as link +from MoinMoin.wikiutil import quoteWikinameURL as quoteURL + +class Theme(ThemeBase): + name = "mentalhealth" + + def editbar(self, d): + """ + Assemble the page edit bar. + + This is rewritten here to get rid of fugly drop-down menu. + (Obviating the need for the actionsMenu() method). + + Also I tried to reduce the number of aliases 'cause I find + that hard to follow. + + @param d: parameter dictionary + @rtype: unicode + @return: iconbar html + """ + # short circuit + if not self.shouldShowEditbar(d['page']): + return '' + + # initialisations & optimisation + _ = self.request.getText + page = d['page'] + cacheKey = 'editbar' + quotedname = quoteURL(page.page_name) + + # use cached copy if possible + cached = self._cache.get(cacheKey) + if cached: + return cached + + # each action in this list is a line on the editbar panel + links = [] + + # parent page + parent = page.getParentPage() + if parent: + links += [parent.link_to(self.request, _("Show Parent", formatted=False))] + + # the rest we will do cleverly :-) + # these are the possible actions and their text labels + choices = [ ['edit', 'edit'], + ['diff', 'show changes'], + ['info', 'get info'], + ['raw', 'show raw text'], + ['print', 'show print view'], + ['refresh', 'delete cache'], + ['AttachFile', 'attach file'], + ['SpellCheck', 'check spelling'], + ['LikePages', 'show like pages'], + ['LocalSiteMap', 'show local site map'], + ['RenamePage', 'rename page'], + ['DeletePage', 'delete page'] + ] + + # determine which actions we can use + available = self.request.getAvailableActions(page) + for action, label in choices: + if action == 'refresh' and not page.canUseCache(): + continue + if action == 'edit' and not (page.isWritable() and self.request.user.may.write(page.page_name)): + continue + + if action[0].isupper() and not action in available: + continue + + links += [link(self.request, '%s?action=%s' % (quotedname, action), + _(label, formatted=False))] + + # we will still delegate this next so I can stop rewriting code + links += [self.subscribeLink(page)] + + # wrap it all up nicely + html = u'

      \n%s\n
    \n' %\ + '\n'.join(['
  • %s
  • ' % item for item in links if item != '']) + + # cache for next call + self._cache[cacheKey] = html + return html + + def header(self, d, **kw): + """ + Assemble page header, which is to say our right-hand panel. + + @param d: parameter dictionary + @rtype: string + @return: page header html + """ + _ = self.request.getText + + # there are 5 main panels: each one follows this markup + html = u'

    %s

    %s
    ' + + # "search" panel hack so I don't have to rewrite searchform() + searchpanel = self.searchform(d).replace('', + self.emit_custom_html(self.cfg.page_header2), + + u'', + + self.msg(d), + self.startPage(), + self.title(d) + ] + return u'\n'.join(parts) + + def footer(self, d, **kw): + """ + Assemble page footer + + @param d: parameter dictionary + @keyword ...:... + @rtype: string + @return: page footer html + """ + parts = [ u'
    ', + self.pageinfo(d['page']), + self.endPage(), + self.emit_custom_html(self.cfg.page_footer1), + self.emit_custom_html(self.cfg.page_footer2), + ] + return u'\n'.join(parts) + +def execute(request): + """ + Generate and return a theme object. + """ + return Theme(request) diff --git a/wiki/theme/monobook.py b/wiki/theme/monobook.py new file mode 100644 index 00000000..a0e77772 --- /dev/null +++ b/wiki/theme/monobook.py @@ -0,0 +1,444 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin monobook theme. Uses the css sheet from + http://wikipedia.org, adapting the moin output to fit it. + + Adapted by Jim Clark + @license: GNU GPL, see COPYING for details. +""" + +from MoinMoin.theme import ThemeBase +from MoinMoin import wikiutil, i18n +from MoinMoin.Page import Page + +class Theme(ThemeBase): + + name = "monobook" + + # Standard set of style sheets + stylesheets = ( + # media basename + ('all', 'common'), + ('screen', 'monobook'), + ('screen', 'screen'), + ('print', 'print'), + ('projection', 'projection'), + ) + + +# Public functions ##################################################### + + def header(self, d, **kw): + """ Assemble wiki header + Here we don't add any menu bars, search bars, etc - instead wait + until the footer. This keeps the HTML cleaner and more accessible, + making sure the main content arrives first. + """ + html = [ + u'
    ', + u'
    ', + self.startPage(), + self.msg(d), + self.title(d), + ] + return u'\n'.join(html) + + def footer(self, d, **keywords): + """ Assemble wiki footer + """ + html = [ + # End of page + u'
    ', + self.endPage(), + u'
    ', + self.columnone(d), + u'
    ' + ] + return u'\n'.join(html) + + def columnone(self, d): + """ assemble all the navigation aids for the page + """ + page = d['page'] + html = [ + u'
    ', + self.editbar(d), + self.username(d), + u'', + self.navibar(d), + self.searchform(d), + self.actionmenu(d), + u'
    ', + u'', + u'
    ' + ] + return u'\n'.join(html) + + def headscript(self, d): + """ Override to not output search/action menu javascript. + (perhaps not a good idea) + """ + return '' + + def extendedAttrs(self, title, accesskey): + """ Helper function for assembling titled access key links + """ + return 'title="%(title)s [alt-%(accesskey)s]" accesskey=%(accesskey)s' % \ + {'accesskey' : accesskey, + 'title' : title} + + def editbar(self, d): + """ Display a list of actions for the page. This list will be turned + into a set of tabbed views on the page by the css. + """ + page = d['page'] + if not self.shouldShowEditbar(page): + return '' + + # Use cached editbar if possible. + cacheKey = 'editbar' + cached = self._cache.get(cacheKey) + if cached: + return cached + + request = self.request + _ = self.request.getText + link = wikiutil.link_tag + quotedname = wikiutil.quoteWikinameURL(page.page_name) + # action, title, description, accesskey + tabs = [('show', 'Article', 'View the page content', 'c'), + ('edit', 'Edit', 'Edit this page', 'e'), + ('diff', 'Show Changes', 'Last page modification', 'd'), + ('info', 'Get Info', 'Page history and information', 'h'), + ('subscribe', 'Subscribe', 'Subscribe to updates to this page', 'w')] + + items = [] + current = self.request.form.get('action', ['show'])[0] + for action, title, description, accesskey in tabs: + if action == current: + cls = 'selected' + else: + cls = 'none' + + if action == 'edit': + if page.isWritable() and request.user.may.write(page.page_name): + pass + else: + action = 'raw' + title = 'View Source' + description = 'This page is protected. You can view its source' + + if action == 'subscribe': + user = self.request.user + if not self.cfg.mail_smarthost or not user.valid: + break + # Email enabled and user valid, get current page status + if user.isSubscribedTo([page.page_name]): + title = 'Unsubscribe' + description = 'Unsubscribe from updates to this page' + + if action == 'show': + url = quotedname + else: + url = quotedname + '?action=' + action + + attrs = self.extendedAttrs(_(description), accesskey) + link = wikiutil.link_tag(self.request, url, _(title), attrs=attrs) + items.append(u'
  • %s
  • ' % (cls, link)) + + html = [ + u'
    ', + u'
      ', + ''.join(items), + u'
    ', + u'
    ' + ] + html = ''.join(html) + # cache for next call + self._cache[cacheKey] = html + + return html + + + def actionmenu(self, d): + """ different implementation of the actionmenu (aka toolbox) + """ + + page = d['page'] + + # Use cached actionmenu if possible. + cacheKey = 'actionmenu' + cached = self._cache.get(cacheKey) + if cached: + return cached + + request = self.request + _ = request.getText + quotedname = wikiutil.quoteWikinameURL(page.page_name) + + menu = [ + 'raw', + 'print', + 'refresh', + 'AttachFile', + 'SpellCheck', + 'LikePages', + 'LocalSiteMap', + 'RenamePage', + 'DeletePage', + ] + + titles = { + 'raw': _('Show Raw Text', formatted=False), + 'print': _('Show Print View', formatted=False), + 'refresh': _('Delete Cache', formatted=False), + 'AttachFile': _('Attach File', formatted=False), + 'SpellCheck': _('Check Spelling', formatted=False), # rename action! + 'RenamePage': _('Rename Page', formatted=False), + 'DeletePage': _('Delete Page', formatted=False), + 'LikePages': _('Show Like Pages', formatted=False), + 'LocalSiteMap': _('Show Local Site Map', formatted=False), + } + + links = [] + + # Format standard actions + available = request.getAvailableActions(page) + for action in menu: + # Enable delete cache only if page can use caching + if action == 'refresh': + if not page.canUseCache(): + break + # Actions which are not available for this wiki, user or page + if action[0].isupper() and not action in available: + break; + + link = wikiutil.link_tag(self.request, \ + quotedname + '?action=' + action, titles[action]) + links.append(link) + + # Add custom actions not in the standard menu + more = [item for item in available if not item in titles] + more.sort() + if more: + # Add more actions (all enabled) + for action in more: + data = {'action': action, 'disabled': ''} + title = Page(request, action).split_title(request, force=1) + # Use translated version if available + title = _(title, formatted=False) + link = wikiutil.link_tag(self.request, \ + quotedname + '?action=' + action, title) + links.append(link) + + html = [ + u'
    ', + u'
    Toolbox
    ', + u'
    ', + u'
      ', + u'
    • %s
    ' % '\n
  • '.join(links), + u'', + u'
  • ', + u'
    ', + ] + html = ''.join(html) + # cache for next call + self._cache[cacheKey] = html + return html + + + def username(self, d): + """ Assemble the username / userprefs link + Copied from the base class, modified to include hotkeys and link titles + """ + from MoinMoin.Page import Page + request = self.request + _ = request.getText + + userlinks = [] + # Add username/homepage link for registered users. We don't care + # if it exists, the user can create it. + if request.user.valid: + homepage = Page(request, request.user.name) + title = homepage.split_title(request) + attrs = self.extendedAttrs(_('User Page'), '.') + homelink = homepage.link_to(request, text=title, attrs=attrs) + userlinks.append(homelink) + + # Set pref page to localized Preferences page + attrs = self.extendedAttrs(_('My Preferences'), 'u') + prefpage = wikiutil.getSysPage(request, 'UserPreferences') + title = prefpage.split_title(request) + userlinks.append(prefpage.link_to(request, text=title, attrs=attrs)) + + # Add a logout link (not sure this is really necessary + attrs = self.extendedAttrs(_('log out'), 'o') + page = d['page'] + url = wikiutil.quoteWikinameURL(page.page_name) + \ + '?action=userform&logout=1' + link = wikiutil.link_tag(self.request, url, 'log out', attrs=attrs) + userlinks.append(link) + + else: + # Add prefpage links with title: Login + prefpage = wikiutil.getSysPage(request, 'UserPreferences') + attrs = self.extendedAttrs('Logging in is not required, but brings benefits', 'o') + userlinks.append(prefpage.link_to(request, text=_("Login"), attrs=attrs)) + + html = [ + u'
    ', + u'
      ', + u'
    • %s
    ' % '\n
  • '.join(userlinks), + u'', + u'
  • ' + ] + return ''.join(html) + + def searchform(self, d): + """ assemble HTML code for the search form + Tweaks from the bass class to wrap in a 'portlet' class, move the + description to a header tag, add an access key of 'f' and + add SearchButton class for the buttons + """ + _ = self.request.getText + form = self.request.form + updates = { + 'search_label' : _('Search:'), + 'search_value': wikiutil.escape(form.get('value', [''])[0], 1), + 'search_full_label' : _('Text', formatted=False), + 'search_title_label' : _('Titles', formatted=False), + } + d.update(updates) + + html = u''' + +''' % d + return html + + def shouldShowEditbar(self, page): + """ Override to include the editbar on edit/preview pages. + (at the risk that the user may accidentally cancel an edit) + """ + if (page.exists(includeDeleted=1) and + self.request.user.may.read(page.page_name)): + return True + return False + + + def navibar(self, d): + """ Alterations from the base class to include access keys and + descriptions for FrontPage/RecentChanges + """ + request = self.request + found = {} # pages we found. prevent duplicates + links = [] # navibar items + current = d['page_name'] + + # Process config navi_bar + if request.cfg.navi_bar: + for text in request.cfg.navi_bar: + pagename, link = self.splitNavilink(text) + if pagename == d['page_front_page']: + attrs = self.extendedAttrs('Visit the main page', 'z') + elif pagename == 'RecentChanges': + attrs = self.extendedAttrs('List of recent changes in this wiki', 'r') + else: + attrs = '' + + a = wikiutil.link_tag(request, pagename, attrs=attrs) + links.append(a) + found[pagename] = 1 + + # Add user links to wiki links, eliminating duplicates. + userlinks = request.user.getQuickLinks() + for text in userlinks: + # Split text without localization, user know what she wants + pagename, link = self.splitNavilink(text, localize=0) + if not pagename in found: + a = wikiutil.link_tag(request, pagename, attrs=attrs) + links.append(a) + found[pagename] = 1 + + html = [ + u'
    ', + u'
    ', + u'' % '\n
  • '.join(links), + u'', + u'
  • ', + u'
    ', + ] + return ''.join(html) + + def pageinfo(self, page): + """ Simple override from base class. + Remove

    so footer isn't too big + """ + _ = self.request.getText + + if self.shouldShowPageinfo(page): + info = page.lastEditInfo() + if info: + if info['editor']: + info = _("last edited %(time)s by %(editor)s") % info + else: + info = _("last modified %(time)s") % info + return info + return '' + + def rtl_stylesheet(self, d): + """ monobook uses a separate css page for rtl alterations. + Add the rtl stylesheet if the user needs it + """ + link = ('') + html = [] + if i18n.getDirection(self.request.lang) == 'rtl': + prefix = self.cfg.url_prefix + href = '%s/%s/css/%s.css' % (prefix, self.name, 'rtl') + html.append(link % (self.stylesheetsCharset, 'all', href)) + return '\n'.join(html) + + def html_head(self, d): + """ Tweak the sending of the head, to include right-to-left + alterations if necessary + """ + html = [ + u'%(title)s - %(sitename)s' % d, + self.headscript(d), # Should move to separate .js file + self.html_stylesheets(d), + self.rtl_stylesheet(d), + self.rsslink(), + ] + return '\n'.join(html) + +def execute(request): + """ + Generate and return a theme object + + @param request: the request object + @rtype: MoinTheme + @return: Theme object + """ + return Theme(request) diff --git a/wiki/theme/movieclub.py b/wiki/theme/movieclub.py new file mode 100644 index 00000000..03892dda --- /dev/null +++ b/wiki/theme/movieclub.py @@ -0,0 +1,33 @@ +# -*- coding: iso-8859-1 -*- +""" + Theme Movieclub, sous classe de Crans. + +""" + + +from crans import ThemeCrans + +class Theme(ThemeCrans): + + + # Standard set of style sheets + stylesheets = ( + # media basename + ('all', 'common'), + ('all', 'common-reverse'), + ('all', 'common-movieclub'), + ('screen', 'crans'), + ('screen', 'movieclub'), + ('print', 'print'), + ('projection', 'projection'), + ) + +def execute(request): + """ + Generate and return a theme object + + @param request: the request object + @rtype: MoinTheme + @return: Theme object + """ + return Theme(request) diff --git a/wiki/theme/rightsidebarsmaller.py b/wiki/theme/rightsidebarsmaller.py new file mode 100644 index 00000000..23ebb78e --- /dev/null +++ b/wiki/theme/rightsidebarsmaller.py @@ -0,0 +1,131 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin theme by and for crw. +""" + +from MoinMoin import wikiutil +from MoinMoin.Page import Page +from MoinMoin.theme import ThemeBase + +class Theme(ThemeBase): + """ here are the functions generating the html responsible for + the look and feel of your wiki site + """ + + name = "rightsidebarsmaller" + + def wikipanel(self, d): + """ Create wiki panel """ + _ = self.request.getText + html = [ + u'

    ', + u'

    %s

    ' % _("Navigation"), + self.navibar(d), + u'
    ', + ] + return u'\n'.join(html) + + def pagepanel(self, d): + """ Create page panel """ + _ = self.request.getText + if self.shouldShowEditbar(d['page']): + html = [ + u'
    ', + u'

    %s

    ' % _("Page"), + self.editbar(d), + u'
    ', + ] + return u'\n'.join(html) + return '' + + def userpanel(self, d): + """ Create user panel """ + _ = self.request.getText + + trail = self.trail(d) + if trail: + trail = u'

    %s

    \n' % _("Recently viewed pages") + trail + + html = [ + u'
    ', + u'

    %s

    ' % _("User"), + self.username(d), + trail, + u'
    ' + ] + return u'\n'.join(html) + + def header(self, d): + """ + Assemble page header + + @param d: parameter dictionary + @rtype: string + @return: page header html + """ + _ = self.request.getText + + html = [ + # Custom html above header + self.emit_custom_html(self.cfg.page_header1), + + # Hedar + u'', + + # Custom html below header (not recomended!) + self.emit_custom_html(self.cfg.page_header2), + + # Sidebar + u'', + + self.msg(d), + + # Page + self.startPage(), + self.title(d), + ] + return u'\n'.join(html) + + def footer(self, d, **keywords): + """ Assemble page footer + + @param d: parameter dictionary + @keyword ...:... + @rtype: string + @return: page footer html + """ + page = d['page'] + html = [ + # Page end + # Used to extend the page to the bottom of the sidebar + u'
    ', + self.pageinfo(page), + self.endPage(), + + # Custom html above footer + self.emit_custom_html(self.cfg.page_footer1), + + # And bellow + self.emit_custom_html(self.cfg.page_footer2), + ] + return u'\n'.join(html) + + +def execute(request): + """ + Generate and return a theme object + + @param request: the request object + @rtype: MoinTheme + @return: Theme object + """ + return Theme(request) + diff --git a/wiki/theme/sinorca4moin.py b/wiki/theme/sinorca4moin.py new file mode 100644 index 00000000..c6808051 --- /dev/null +++ b/wiki/theme/sinorca4moin.py @@ -0,0 +1,230 @@ +# -*- coding: iso-8859-1 -*- +"""MoinMoin theme "sinorca4moin" by David Linke. + +Credits to "Haran" who published his sinorca-design at www.oswd.org +""" + +from MoinMoin import wikiutil +from MoinMoin.Page import Page +from MoinMoin.theme import ThemeBase + +class Theme(ThemeBase): + """ here are the functions generating the html responsible for + the look and feel of your wiki site + """ + + name = "sinorca4moin" + + def iconbar(self, d): + """ + Assemble the iconbar + + @param d: parameter dictionary + @rtype: string + @return: iconbar html + """ + iconbar = [] + if self.cfg.page_iconbar and self.request.user.show_toolbar and d['page_name']: + iconbar.append('
      \n') + icons = self.cfg.page_iconbar[:] + for icon in icons: + if icon == "up": + if d['page_parent_page']: + iconbar.append('
    • %s
    • \n' % self.make_iconlink(icon, d)) + elif icon == "subscribe": + iconbar.append('
    • %s
    • \n' % self.make_iconlink( + ["subscribe", "unsubscribe"][self.request.user.isSubscribedTo([d['page_name']])], d)) + elif icon == "home": + if d['page_home_page']: + iconbar.append('
    • %s
    • \n' % self.make_iconlink(icon, d)) + else: + iconbar.append('
    • %s
    • \n' % self.make_iconlink(icon, d)) + iconbar.append('
    \n') + return ''.join(iconbar) + + def editbar(self, d): + """ Assemble the page edit bar. + + Display on existing page. Replace iconbar, showtext, edit text, + refresh cache and available actions. + + @param d: parameter dictionary + @rtype: unicode + @return: iconbar html + """ + page = d['page'] + if not self.shouldShowEditbar(page): + return '' + + # Use cached editbar if possible. + cacheKey = 'editbar' + cached = self._cache.get(cacheKey) + if cached: + return cached + + # Make new edit bar + request = self.request + _ = self.request.getText + link = wikiutil.link_tag + quotedname = wikiutil.quoteWikinameURL(page.page_name) + links = [] + add = links.append + + # Parent page + #parent = page.getParentPage() + #if parent: + # add(parent.link_to(request, _("Show Parent", formatted=False))) + + # Page actions + if page.isWritable() and request.user.may.write(page.page_name): + add(link(request, quotedname + '?action=edit', _('Edit'))) + else: + add(_('Immutable Page', formatted=False)) + + add(link(request, quotedname + '?action=info', + _('Get Info', formatted=False))) + add(self.actionsMenu(page)) + + # Format + items = '\n'.join(['
  • %s
  • ' % item for item in links if item != '']) + html = u'
      \n%s\n
    \n' % items + + # cache for next call + self._cache[cacheKey] = html + return html + + def wikipanel(self, d): + """ Create wiki panel """ + _ = self.request.getText + html = [ + u'
    ', + u'

    %s

    ' % _("Wiki"), + self.navibar(d), + u'
    ', + ] + return u'\n'.join(html) + + def pagepanel(self, d): + """ Create page panel """ + _ = self.request.getText + if self.shouldShowEditbar(d['page']): + html = [ + u'
    ', + u'

    %s

    ' % _("Page"), + self.editbar(d), + u'
    ', + ] + return u'\n'.join(html) + return '' + + def userpanel(self, d): + """ Create user panel """ + _ = self.request.getText + + html = [ + u'
    ', + u'

    %s

    ' % _("User"), + self.username(d), + u'
    ' + ] + return u'\n'.join(html) + + def logo(self): + """ Assemble logo with link to front page + + adds h1-tags for sinorca + """ + if self.cfg.logo_string: + pagename = wikiutil.getFrontPage(self.request).page_name + pagename = wikiutil.quoteWikinameURL(pagename) + logo = wikiutil.link_tag(self.request, pagename, self.cfg.logo_string) + html = u'''''' % logo + return html + return u'' + + def header(self, d): + """ + Assemble page header + + @param d: parameter dictionary + @rtype: string + @return: page header html + """ + _ = self.request.getText + + trail = self.trail(d) + + html = [ + # Header + u'', + + # Iconbar + self.iconbar(d), + + # Sidebar + u'', + u'', + + self.msg(d), + + # Page + self.startPage(), + self.title(d), + ] + return u'\n'.join(html) + + def footer(self, d, **keywords): + """ Assemble page footer + + @param d: parameter dictionary + @keyword ...:... + @rtype: string + @return: page footer html + """ + page = d['page'] + html = [ + # Page end + # Used to extend the page to the bottom of the sidebar + u'
    ', + self.pageinfo(page), + self.endPage(), + + # Custom html above footer + self.emit_custom_html(self.cfg.page_footer1), + + # And bellow + self.emit_custom_html(self.cfg.page_footer2), + ] + return u'\n'.join(html) + + +def execute(request): + """ + Generate and return a theme object + + @param request: the request object + @rtype: MoinTheme + @return: Theme object + """ + return Theme(request) +