[isabelle] Font substitution and handling in Isabelle/jEdit



As you may know, the current font handling system in the Isabelle plugin
completely bypasses the one used in jEdit.
The symptoms of this are:
 - Global Options -> Text Area -> "Additional fonts with font
   substitution" has no effect, confusing new users.
 - Can't use Your Favorite Fontâ and have missing glyphs fall
   back to the fonts you want.
 - Isabelle symbols can configure the font used, but there's a global
   maximum of two user fonts (see user_font in token_markup.scala).
 - jEdit splits chunks at *all* Isabelle symbols

jEdit's font substitution system works really well. I wanted
Isabelle users to be able to benefit from it and enjoy tweaking fonts to
their heart's content, so I performed the necessary modifications to
make it work. The resulting code is simpler (though obviously Makarius
will tune naming to be more canonical) and I believe more
understandable.

I promised last week I'd deliver the change this week, and here it is.
I've also included some ad-hoc documentation on the situation and design
in the latter part of this email, hence its length. If you ever want to
tune the symbol/token/chunk system, this might be useful.

If you want to try it out, two patches are attached (use git apply):

1. jEdit: 0003-Allow-specification-of-sizes-for-fallback-fonts.patch
   (see discussion at: https://sourceforge.net/p/jedit/patches/569/ )

   This allows one to specify desired sizes for fallback fonts. The
   reality is that font metrics between fonts don't agree. One font at
   12 pt is the same size as another at 16 pt.

   It also exposes queries on the font substitution system in Chunk.java
   so that the plugin authors can use the information for their own
   rendering.

   This patch on its own does *not* affect existing jEdit/Isabelle users
   and is safe to apply.

   If you just want my version of jedit.jar, let me know.

2. Isabelle plugin: 0001-Redo-Isabelle-plugin-chunk-rendering.patch
   (Isabelle 2015 version, but I can easily produce one that applies to
   current development branch)

   This removes the user_font setup in token_markup.scala and
   modifies the paint_chunk_list code in rich_text_area.scala to match
   the underlying chunk layout provided by jEdit even when font
   substitution is enabled.

   This depends on the jEdit patch (1).


The Outcome

You can now set up whatever font chain you want. For example mine is:
Lucida Console 16 -> Cambria Math 18 -> DejaVu Sans 16 -> IsabelleText
16 -> search all system fonts.

The setup is flexible enough that you can now use bizarre Unicode code
points (yes, even those above 0xFFFF) in your notation (output) if you
have the fonts to display it, e.g. I tested with:
    U+1F0A4 PLAYING CARD FOUR OF SPADES
    U+10147 GREEK ACROPHONIC ATTIC FIFTY THOUSAND

The chunk rendering code in rich_text_area.scala is simpler and should
be easier to understand.

By not using the user_font workaround, we free up 19*2 extended style
IDs that we can use for whatever we like. My suggestion: we can now do
*two* levels of super/subscript.

The "font" specification in etc/symbols now does nothing. Pick the
Unicode code point you want the symbol to display as and you're done.


The Details (only read if you are interested or Makarius)

jEdit's TextArea lays out chunks based on tokens it receives from the
relevant parser. Consecutive tokens with the same ID are grouped into
one chunk.  One chunk has a single style (font, text color, background
color) plus a layout of GlyphVectors. If no font substitution was done,
the entire chunk is laid out as a single vector. Font substitution
splits vectors when the chunk is laid out, but the chunk is not split.

The Isabelle plugin provides a parser which lets jEdit parse and lay out
chunks. This results in inner syntax appearing as LITERAL1 (i.e. a
string). Once this process is done, all chunks are laid out, have
specified sizes and all font substitutions are done.

That's not the end of the story though. Once processed by Isabelle, we
acquire extra information. For example a part of that LITERAL1 is
actually a free variable, so should get a different text color! This
needs to be overlaid on the existing chunk somehow.

The way the Isabelle plugin performs the overlay of text colors is by
turning off the existing TextArea chunk renderer and doing the rendering
itself (see rich_text_area.scala). However, it isn't dumb and does not
lay out the underlying chunks again. This means that the rendering must
match up with the underlying chunk layout *exactly* in order to have the
glyphs and cursor appear in the right place within the chunk.

So if we want sub/superscript, we need to get jEdit to lay it out as
sub/superscript for us, or rendering our own ideas over it won't match
up!

There is a patch to jEdit (src/Tools/jEdit/patches/extended_styles)
which extends the n jEdit styles available (about 19) to 127 (max in
Java byte). That way we can do:
 - 0..n-1     : jEdit
 - n..2*n-1   : superscript
 - 2*n..3*n-1 : subscript
 - 3*n..4*n-1 : bold
 - 4*n..6*n-1 : user_font
 - 6*n        : invisible (e.g. \<^sub> token itself)
When jEdit sees these, it lays them out according to the extended style
specified. We set these up to match the n jEdit styles, but tweaked in
the desired way (size, font, style, elevation, etc).

The user_font setup handles Isabelle symbols, but it's not very flexible
and it unnecessarily splits chunks (e.g. "sÎn â mÏÏn" lays out as 9
chunks instead of one).
The renderer in Rich_Text_Area assumes one font per chunk, and user_font
only handles Isabelle symbols and not general code points present in the
text. The result is that with font substitution enabled in jEdit, using
code points not in your main font (yes, even IsabelleText has these)
will also result in glyph rendering misalignment.

As Isabelle plugin developers we cannot reuse the internal glyph
vectors stored in the chunk for rendering our text colors, because if
all code points are present in your main font, then "sÎn â mÏÏn" will
not only be a single chunk, but also a single glyph vector. After it's
assembled, we can't get at which part means what.

So what do we do instead?

Create an AttributeString to contain all the rendering information for
the chunk. AttributeStrings are great. They store all style information
we need, but also font information.

For each Isabelle text color in the chunk, mark that range in the
AttributeString with that color, defaulting to chunk's default text
color and font. Mark where the cursor is as inverted-color.

Access the font lookup mechanism in Chunk.java (possible with the
patch).  If jEdit didn't do font substitution, we don't either.
Otherwise: go through the AttributeString looking for code points the
chunk's font can't display. Ask Chunk if there's a substitute font to
use to display it. If there is, mark the range of the code point in the
AttributeString with that font.

Render resulting AttributeString. Watch as it lines up perfectly with
jEdit's layout regardless of what code points you dug out from the
depths of Unicode.

The end. It really is simpler, and I think a good change for Isabelle to
adopt.

Makarius: I know you probably won't like CustomChunk, or at the very
least its name. I just wanted to encapsulate the AttributeString
operations for simplicity and understandability. As always, if you're
not happy with something and want me to change/redo, let me know. I want
to make useful contributions.

Sincerely,

Rafal Kolanski

>From c344cb9219269f68c535422d556e1e10644a9f21 Mon Sep 17 00:00:00 2001
From: Rafal Kolanski <xs at xaph.net>
Date: Fri, 28 Aug 2015 13:20:39 +1000
Subject: [PATCH] Allow specification of sizes for fallback fonts.

Not all fonts are the same pixel size for a given point size and
presumably the user wants the final metrics to match.

Given that now the user can specify preferred fallback font sizes, that
leaves system-fallback fonts, which don't come with size info. Since
java.awt.GraphicsEnvironment.getAllFonts returns a font size of 1, we
scale those fonts up to the main font point size.

Font replacement also preserves any affine transforms on the original
font.

So if you know what you're doing, you can set up fonts precisely the way
you like, and if you don't, you can just select the system font fallback
behavior and get the previous outcome.
---
 org/gjt/sp/jedit/options/TextAreaOptionPane.java |   3 +-
 org/gjt/sp/jedit/syntax/Chunk.java               | 110 ++++++++++++++++-------
 2 files changed, 79 insertions(+), 34 deletions(-)

diff --git a/org/gjt/sp/jedit/options/TextAreaOptionPane.java b/org/gjt/sp/jedit/options/TextAreaOptionPane.java
index 7fb0277..202097c 100644
--- a/org/gjt/sp/jedit/options/TextAreaOptionPane.java
+++ b/org/gjt/sp/jedit/options/TextAreaOptionPane.java
@@ -410,7 +410,6 @@ public class TextAreaOptionPane extends AbstractOptionPane
 
 				if (selected != null)
 				{
-					selected = selected.deriveFont(Font.PLAIN, 12);
 					fontsModel.addElement(selected);
 					fonts.setSelectedIndex(fontsModel.size() - 1);
 				}
@@ -490,7 +489,7 @@ public class TextAreaOptionPane extends AbstractOptionPane
 								   index,
 								   isSelected,
 								   cellHasFocus);
-				setText(f.getFamily());
+				setText(f.getFamily() + " " + f.getSize());
 				return this;
 			}
 
diff --git a/org/gjt/sp/jedit/syntax/Chunk.java b/org/gjt/sp/jedit/syntax/Chunk.java
index c8fd33f..ebd3eff 100644
--- a/org/gjt/sp/jedit/syntax/Chunk.java
+++ b/org/gjt/sp/jedit/syntax/Chunk.java
@@ -34,6 +34,7 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 import java.lang.ref.SoftReference;
 
+import org.gjt.sp.jedit.jEdit;
 import org.gjt.sp.jedit.Debug;
 import org.gjt.sp.jedit.IPropertyManager;
 //}}}
@@ -241,9 +242,9 @@ public class Chunk extends Token
 				 * doesn't match any installed fonts. The following
 				 * check skips fonts that don't exist.
 				 */
-				Font f = new Font(family, Font.PLAIN, 12);
-				if (!"dialog".equalsIgnoreCase(f.getFamily()) ||
-					"dialog".equalsIgnoreCase(family))
+				Font f = jEdit.getFontProperty("view.fontSubstList." + i);
+				if (f != null && (!"dialog".equalsIgnoreCase(f.getFamily()) ||
+						"dialog".equalsIgnoreCase(f.getFamily())))
 					userFonts.add(f);
 				i++;
 			}
@@ -256,6 +257,67 @@ public class Chunk extends Token
 		glyphCache = null;
 	} //}}}
 
+	//{{{ getSubstFont() method
+	/**
+	 * Returns the first font which can display a character from
+	 * configured substitution candidates, or null if there is no
+	 * such font.
+	 */
+	public static Font getSubstFont(int codepoint)
+	{
+		// Workaround for a problem reported in SF.net patch #3480246
+		// > If font substitution with system fonts is enabled,
+		// > I get for inserted control characters strange mathematical
+		// > symbols from a non-unicode font in my system.
+		if (Character.isISOControl(codepoint))
+			return null;
+
+		for (Font candidate: getFontSubstList())
+		{
+			if (candidate.canDisplay(codepoint))
+			{
+				return candidate;
+			}
+		}
+		return null;
+	} //}}}
+
+	//{{{ deriveSubstFont() method
+	/**
+	 * Derives a font to match the main font for purposes of
+	 * font substitution.
+	 * Preserves any transformations from main font.
+	 * For system-fallback fonts, derives size and style from main font.
+	 *
+	 * @param mainFont Font to derive from
+	 * @param candidateFont Font to transform
+	 */
+	public static Font deriveSubstFont(Font mainFont, Font candidateFont)
+	{
+		// adopt subst font family and size, but preserve any transformations
+		// i.e. if font is squashed/sheared, subst font glyphs should be squashed
+		Font substFont = candidateFont.deriveFont(mainFont.getTransform());
+
+		// scale up system fonts (point size 1) to size of main font
+		if (substFont.getSize() == 1)
+			substFont = substFont.deriveFont(mainFont.getStyle(),
+				mainFont.getSize());
+
+		return substFont;
+	} //}}}
+
+	//{{{ usedFontSubstitution() method
+	/**
+	 * Returns true if font substitution was used in the layout of this chunk.
+	 * If substitution was not used, the chunk may be assumed to be composed
+	 * of one glyph using a single font.
+	 */
+	public boolean usedFontSubstitution()
+	{
+		return (fontSubstEnabled && glyphs != null && glyphs.length > 1);
+	}
+	//}}}
+
 	//{{{ Package private members
 
 	//{{{ Instance variables
@@ -514,6 +576,14 @@ public class Chunk extends Token
 	//}}}
 
 	//{{{ getFontSubstList() method
+	/**
+	 * Obtain a list of preferred fallback fonts as specified by the user
+	 * (see Text Area in Global Options), as well as a list of all fonts
+	 * specified in the system.
+	 * Note that preferred fonts are returned with sizes as specified by the
+	 * user, but system fonts all have a point size of 1. These should be
+	 * scaled up once the main font is known (see layoutGlyphs()).
+	 */
 	private static Font[] getFontSubstList()
 	{
 		if (fontSubstList == null)
@@ -543,31 +613,6 @@ public class Chunk extends Token
 		return fontSubstList;
 	} //}}}
 
-	//{{{ getSubstFont() method
-	/**
-	 * Returns the first font which can display a character from
-	 * configured substitution candidates, or null if there is no
-	 * such font.
-	 */
-	private static Font getSubstFont(int codepoint)
-	{
-		// Workaround for a problem reported in SF.net patch #3480246
-		// > If font substitution with system fonts is enabled,
-		// > I get for inserted control characters strange mathematical
-		// > symbols from a non-unicode font in my system.
-		if (Character.isISOControl(codepoint))
-			return null;
-
-		for (Font candidate: getFontSubstList())
-		{
-			if (candidate.canDisplay(codepoint))
-			{
-				return candidate;
-			}
-		}
-		return null;
-	} //}}}
-
 	//{{{ drawGlyphs() method
 	/**
 	 * Draws the internal list of glyph vectors into the given
@@ -663,8 +708,10 @@ public class Chunk extends Token
 			int charCount = Character.charCount(nextChar);
 			assert !mainFont.canDisplay(nextChar);
 			Font substFont = getSubstFont(nextChar);
+
 			if (substFont != null)
 			{
+				substFont = deriveSubstFont(mainFont, substFont);
 				subst.addRange(substFont, charCount);
 			}
 			else
@@ -758,10 +805,9 @@ public class Chunk extends Token
 			{
 				return;
 			}
-			Font font = (rangeFont == null) ?
-				mainFont :
-				rangeFont.deriveFont(mainFont.getStyle(),
-					mainFont.getSize());
+
+			Font font = (rangeFont == null) ? mainFont : rangeFont;
+
 			GlyphVector gv = layoutGlyphVector(font, frc,
 				text, rangeStart, rangeStart + rangeLength);
 			glyphs.add(gv);
-- 
2.1.0

>From c12a888e3269247ba4ff583cd31d68967f7ad7ec Mon Sep 17 00:00:00 2001
From: Rafal Kolanski <rafal.kolanski at nicta.com.au>
Date: Wed, 2 Sep 2015 10:27:12 +1000
Subject: [PATCH] Redo Isabelle plugin chunk rendering.

(depends on jedit patch
 0003-Allow-specification-of-sizes-for-fallback-fonts.patch,
 see: https://sourceforge.net/p/jedit/patches/569/ )

Use AttributeString and jEdit's information on font substitution.
Simplify code greatly, use existing jEdit options to set up the fonts,
get rid of max_user_fonts = 2 restriction, and free up 19*2 slots in the
extended styles table (which can then be used for a second layer of
sub&superscript).

Font specification in etc/symbols is now ignored.

Tested with missing glyphs, user font fallbacks, symbol font fallbacks
and code points above 0xFFFF (take up two or more chars in Java).
Checked that overlap with jEdit internal chunk glyphs is pixel-perfect.
---
 src/Tools/jEdit/src/rich_text_area.scala | 108 ++++++++++++++++++-------------
 src/Tools/jEdit/src/token_markup.scala   |  19 +-----
 2 files changed, 67 insertions(+), 60 deletions(-)

diff --git a/src/Tools/jEdit/src/rich_text_area.scala b/src/Tools/jEdit/src/rich_text_area.scala
index 66a23a7..2afb5d7 100644
--- a/src/Tools/jEdit/src/rich_text_area.scala
+++ b/src/Tools/jEdit/src/rich_text_area.scala
@@ -10,7 +10,7 @@ package isabelle.jedit
 
 import isabelle._
 
-import java.awt.{Graphics2D, Shape, Color, Point, Toolkit, Cursor, MouseInfo}
+import java.awt.{Graphics2D, Shape, Color, Point, Toolkit, Cursor, MouseInfo, Font}
 import java.awt.event.{MouseMotionAdapter, MouseAdapter, MouseEvent,
   FocusAdapter, FocusEvent, WindowEvent, WindowAdapter, InputEvent}
 import java.awt.font.TextAttribute
@@ -357,6 +357,50 @@ class Rich_Text_Area(
     else rendering.caret_invisible_color
   }
 
+  private class CustomChunk(line_start: Text.Offset, chunk: Chunk)
+  {
+    val offset : Text.Offset = line_start + chunk.offset
+    val str : String = if (chunk.str == null) " " * chunk.length else chunk.str
+    val astr : AttributedString = new AttributedString(str)
+
+    astr.addAttribute(TextAttribute.FONT, chunk.style.getFont)
+    astr.addAttribute(TextAttribute.FOREGROUND, chunk.style.getForegroundColor)
+
+    def addAttributeRange(range: Text.Range, attr: TextAttribute, o: Object) =
+      astr.addAttribute(attr, o, range.start - offset, range.stop - offset)
+
+    def addTextColorRange(range: Text.Range, color: Color) =
+      addAttributeRange(range, TextAttribute.FOREGROUND, color)
+
+    def draw(gfx: Graphics2D, x: Float , y: Float) =
+      gfx.drawString(astr.getIterator, x, y)
+
+    def deriveSubstFont(codepoint: Int): Font =
+    {
+      val mainFont = chunk.style.getFont()
+      Chunk.getSubstFont(codepoint) match {
+        case substFont: Font => Chunk.deriveSubstFont(mainFont, substFont)
+        case _ => mainFont
+      }
+    }
+
+    def doSubstFonts()
+    {
+      if (chunk.usedFontSubstitution()) {
+        val mainFont = chunk.style.getFont
+        var i = mainFont.canDisplayUpTo(astr.getIterator(), 0, str.length)
+
+        while (i >= 0 && i < str.length)
+        {
+          val next = str.offsetByCodePoints(i, 1)
+          val substFont = deriveSubstFont(str.codePointAt(i))
+          astr.addAttribute(TextAttribute.FONT, substFont, i, next)
+          i = mainFont.canDisplayUpTo(astr.getIterator(), next, str.length)
+        }
+      }
+    }
+  }
+
   private def paint_chunk_list(rendering: Rendering,
     gfx: Graphics2D, line_start: Text.Offset, head: Chunk, x: Float, y: Float): Float =
   {
@@ -370,66 +414,42 @@ class Rich_Text_Area(
 
     var w = 0.0f
     var chunk = head
+
     while (chunk != null) {
-      val chunk_offset = line_start + chunk.offset
+
       if (x + w + chunk.width > clip_rect.x &&
           x + w < clip_rect.x + clip_rect.width && chunk.length > 0)
       {
-        val chunk_range = Text.Range(chunk_offset, chunk_offset + chunk.length)
-        val chunk_str = if (chunk.str == null) " " * chunk.length else chunk.str
-        val chunk_font = chunk.style.getFont
-        val chunk_color = chunk.style.getForegroundColor
+        val cc = new CustomChunk(line_start, chunk)
 
-        def string_width(s: String): Float =
-          if (s.isEmpty) 0.0f
-          else chunk_font.getStringBounds(s, font_context).getWidth.toFloat
+        val chunk_offset = line_start + chunk.offset
+        val chunk_range = Text.Range(chunk_offset, chunk_offset + chunk.length)
 
         val markup =
           for {
-            r1 <- rendering.text_color(chunk_range, chunk_color)
+            r1 <- rendering.text_color(chunk_range, chunk.style.getForegroundColor)
             r2 <- r1.try_restrict(chunk_range)
           } yield r2
 
-        val padded_markup_iterator =
-          if (markup.isEmpty)
-            Iterator(Text.Info(chunk_range, chunk_color))
-          else
-            Iterator(
-              Text.Info(Text.Range(chunk_range.start, markup.head.range.start), chunk_color)) ++
-            markup.iterator ++
-            Iterator(Text.Info(Text.Range(markup.last.range.stop, chunk_range.stop), chunk_color))
-
-        var x1 = x + w
-        gfx.setFont(chunk_font)
-        for (Text.Info(range, color) <- padded_markup_iterator if !range.is_singularity) {
-          val str = chunk_str.substring(range.start - chunk_offset, range.stop - chunk_offset)
-          gfx.setColor(color)
+        // mark Isabelle colors
+        for (Text.Info(range, color) <- markup.iterator if !range.is_singularity)
+        {
+          cc.addTextColorRange(range, color)
 
+          // flip colors in cursor
           range.try_restrict(caret_range) match {
-            case Some(r) if !r.is_singularity =>
-              val i = r.start - range.start
-              val j = r.stop - range.start
-              val s1 = str.substring(0, i)
-              val s2 = str.substring(i, j)
-              val s3 = str.substring(j)
-
-              if (s1.nonEmpty) gfx.drawString(s1, x1, y)
-
-              val astr = new AttributedString(s2)
-              astr.addAttribute(TextAttribute.FONT, chunk_font)
-              astr.addAttribute(TextAttribute.FOREGROUND, caret_color(rendering))
-              astr.addAttribute(TextAttribute.SWAP_COLORS, TextAttribute.SWAP_COLORS_ON)
-              gfx.drawString(astr.getIterator, x1 + string_width(s1), y)
-
-              if (s3.nonEmpty)
-                gfx.drawString(s3, x1 + string_width(str.substring(0, j)), y)
-
+            case Some(r) if !r.is_singularity => {
+              cc.addAttributeRange(r, TextAttribute.FOREGROUND, caret_color(rendering))
+              cc.addAttributeRange(r, TextAttribute.SWAP_COLORS, TextAttribute.SWAP_COLORS_ON)
+            }
             case _ =>
-              gfx.drawString(str, x1, y)
           }
-          x1 += string_width(str)
         }
+
+        cc.doSubstFonts()
+        cc.draw(gfx, x + w, y)
       }
+
       w += chunk.width
       chunk = chunk.next.asInstanceOf[Chunk]
     }
diff --git a/src/Tools/jEdit/src/token_markup.scala b/src/Tools/jEdit/src/token_markup.scala
index 9c6edcd..9c941fd 100644
--- a/src/Tools/jEdit/src/token_markup.scala
+++ b/src/Tools/jEdit/src/token_markup.scala
@@ -74,7 +74,6 @@ object Token_Markup
   def subscript(i: Byte): Byte = { check_range(i); (i + plain_range).toByte }
   def superscript(i: Byte): Byte = { check_range(i); (i + 2 * plain_range).toByte }
   def bold(i: Byte): Byte = { check_range(i); (i + 3 * plain_range).toByte }
-  def user_font(idx: Int, i: Byte): Byte = { check_range(i); (i + (4 + idx) * plain_range).toByte }
   val hidden: Byte = (6 * plain_range).toByte
 
   private def font_style(style: SyntaxStyle, f: Font => Font): SyntaxStyle =
@@ -107,11 +106,6 @@ object Token_Markup
 
   class Style_Extender extends SyntaxUtilities.StyleExtender
   {
-    val max_user_fonts = 2
-    if (Symbol.font_names.length > max_user_fonts)
-      error("Too many user symbol fonts (" + max_user_fonts + " permitted): " +
-        Symbol.font_names.mkString(", "))
-
     override def extendStyles(styles: Array[SyntaxStyle]): Array[SyntaxStyle] =
     {
       val new_styles = new Array[SyntaxStyle](full_range)
@@ -127,16 +121,13 @@ object Token_Markup
         new_styles(subscript(i.toByte)) = script_style(style, -1)
         new_styles(superscript(i.toByte)) = script_style(style, 1)
         new_styles(bold(i.toByte)) = bold_style(style)
-        for (idx <- 0 until max_user_fonts)
-          new_styles(user_font(idx, i.toByte)) = style
-        for ((family, idx) <- Symbol.font_index)
-          new_styles(user_font(idx, i.toByte)) = font_style(style, GUI.imitate_font(_, family))
       }
+
       new_styles(hidden) =
         new SyntaxStyle(hidden_color, null,
           { val font = styles(0).getFont
-            GUI.transform_font(new Font(font.getFamily, 0, 1),
-              AffineTransform.getScaleInstance(1.0, font.getSize.toDouble)) })
+            GUI.transform_font(new Font(font.getFamily, 0, font.getSize),
+              AffineTransform.getScaleInstance(1.0/font.getSize.toDouble, 1.0)) })
       new_styles
     }
   }
@@ -166,10 +157,6 @@ object Token_Markup
         }
         control = ""
       }
-      Symbol.lookup_font(sym) match {
-        case Some(idx) => mark(offset, offset + sym.length, user_font(idx, _))
-        case _ =>
-      }
       offset += sym.length
     }
     result
-- 
2.1.0



This archive was generated by a fusion of Pipermail (Mailman edition) and MHonArc.