diff --git a/ZConfig/info.py b/ZConfig/info.py
index 02ced04..758430b 100644
--- a/ZConfig/info.py
+++ b/ZConfig/info.py
@@ -17,6 +17,7 @@ import copy
 import ZConfig
 
 from abc import abstractmethod
+from collections import OrderedDict
 from functools import total_ordering
 
 from ZConfig._compat import AbstractBaseClass
@@ -137,7 +138,7 @@ class BaseKeyInfo(AbstractBaseClass, BaseInfo):
         assert self.name == "+"
         if self._rawdefaults is None:
             self._rawdefaults = self._default
-        self._default = {}
+        self._default = OrderedDict()
 
 
 class KeyInfo(BaseKeyInfo):
@@ -148,7 +149,7 @@ class KeyInfo(BaseKeyInfo):
         BaseKeyInfo.__init__(self, name, datatype, minOccurs, 1,
                              handler, attribute)
         if self.name == "+":
-            self._default = {}
+            self._default = OrderedDict()
 
     def add_valueinfo(self, vi, key):
         if self.name == "+":
@@ -184,7 +185,7 @@ class MultiKeyInfo(BaseKeyInfo):
         BaseKeyInfo.__init__(self, name, datatype, minOccurs, maxOccurs,
                              handler, attribute)
         if self.name == "+":
-            self._default = {}
+            self._default = OrderedDict()
         else:
             self._default = []
 
@@ -269,14 +270,19 @@ class SectionInfo(BaseInfo):
 
 
 class AbstractType(object):
-
+    # This isn't actually "abstract" in the Python ABC sense,
+    # it's only abstract from the schema sense. This class is
+    # instantiated, and not expected to be subclassed.
     __slots__ = '_subtypes', 'name', 'description'
 
     def __init__(self, name):
-        self._subtypes = {}
+        self._subtypes = OrderedDict()
         self.name = name
         self.description = None
 
+    def __iter__(self):
+        return iter(self._subtypes.items())
+
     def addsubtype(self, type):
         self._subtypes[type.name] = type
 
@@ -314,8 +320,8 @@ class SectionType(object):
         self.example = None
         self.registry = registry
         self._children = []    # [(key, info), ...]
-        self._attrmap = {}     # {attribute: info, ...}
-        self._keymap = {}      # {key: info, ...}
+        self._attrmap = OrderedDict()     # {attribute: info, ...}
+        self._keymap = OrderedDict()      # {key: info, ...}
         self._types = types
 
     def gettype(self, name):
@@ -334,6 +340,12 @@ class SectionType(object):
     def __getitem__(self, index):
         return self._children[index]
 
+    def __iter__(self):
+        return iter(self._children)
+
+    def itertypes(self):
+        return iter(sorted(self._types.items()))
+
     def _add_child(self, key, info):
         # check naming constraints
         assert key or info.attribute
@@ -367,7 +379,7 @@ class SectionType(object):
             raise ZConfig.ConfigurationError("no key matching " + repr(key))
 
     def getrequiredtypes(self):
-        d = {}
+        d = OrderedDict()
         if self.name:
             d[self.name] = 1
         stack = [self]
@@ -432,7 +444,7 @@ class SchemaType(SectionType):
                  registry):
         SectionType.__init__(self, None, keytype, valuetype, datatype,
                              registry, {})
-        self._components = {}
+        self._components = OrderedDict()
         self.handler = handler
         self.url = url
 
diff --git a/ZConfig/schema2html.py b/ZConfig/schema2html.py
index c3cc33c..0d0791c 100755
--- a/ZConfig/schema2html.py
+++ b/ZConfig/schema2html.py
@@ -22,6 +22,7 @@ import ZConfig.loader
 from ZConfig.info import SectionType
 from ZConfig.info import SectionInfo
 from ZConfig.info import ValueInfo
+from ZConfig.info import AbstractType
 
 def esc(x):
     return cgi.escape(str(x))
@@ -32,7 +33,10 @@ def dt(x):
     if isinstance(x, type):
         return '%s %s' % (tn, x.__module__ + '.' + x.__name__)
 
-    return '%s %s' % (tn, x.__name__)
+    if hasattr(x, '__name__'):
+        return '%s %s' % (tn, x.__name__)
+
+    return tn
 
 class explain(object):
     done = []
@@ -88,6 +92,13 @@ def printContents(name, info, file=None):
             for sub in info.sectiontype:
                 printContents(*sub, file=out)
             print('</dl></dd>', file=out)
+    elif isinstance(info, AbstractType):
+        print('<dt><b><i>', info.name, '</i></b>', file=out)
+        print('<dd>', file=out)
+        if info.description:
+            print(info.description, file=out) # pragma: no cover
+        explain(info, file=out)
+        print('</dd>', file=out)
     else:
         print('<dt><b>',info.name, '</b>', '(%s)' % dt(info.datatype), file=out)
         default = info.getdefault()
@@ -108,26 +119,47 @@ def main(argv=None):
         description="Print an HTML version of a schema")
     argparser.add_argument(
         "schema",
-        help="The schema to print",
-        default="-",
-        type=argparse.FileType('r'))
+        metavar='[SCHEMA-OR-PACKAGE]',
+        help="The schema to print. By default, a file. Optionally, a Python package."
+             " If not given, defaults to reading a schema file from stdin",
+        default="-"
+        )
     argparser.add_argument(
         "--out", "-o",
         help="Write the schema to this file; if not given, write to stdout",
         type=argparse.FileType('w'))
+    argparser.add_argument(
+        "--package",
+        action='store_true',
+        default=False,
+        help="The SCHEMA-OR-PACKAGE argument indicates a Python package instead of a file."
+             " The component.xml (by default) from the package will be read.")
+    argparser.add_argument(
+        "--package-file",
+        action="store",
+        default="component.xml",
+        help="When PACKAGE is given, this can specify the file inside it to load.")
 
     args = argparser.parse_args(argv)
 
     out = args.out or sys.stdout
 
-    schema = ZConfig.loader.loadSchemaFile(args.schema)
+    if not args.package:
+        schema_reader = argparse.FileType('r')(args.schema)
+    else:
+        schema_template = "<schema><import package='%s' file='%s' /></schema>" % (
+            args.schema, args.package_file)
+        from ZConfig._compat import TextIO
+        schema_reader = TextIO(schema_template)
+
+    schema = ZConfig.loader.loadSchemaFile(schema_reader)
 
     print('''<html><body>
     <style>
     dl {margin: 0 0 1em 0;}
     </style>
     ''', file=out)
-    printSchema(schema, out)
+    printSchema(iter(schema) if not args.package else schema.itertypes(), out)
     print('</body></html>', file=out)
 
     return 0

zconfig_schema2html should handle component.xml from packages
If you feed it a component file, right now it blows up. If I change the script to do the obvious thing and wrap a `<schema><import package="..."/></schema>` around it, then you get no output.
diff --git a/ZConfig/tests/test_schema2html.py b/ZConfig/tests/test_schema2html.py
index b11eb31..f1cf668 100644
--- a/ZConfig/tests/test_schema2html.py
+++ b/ZConfig/tests/test_schema2html.py
@@ -49,7 +49,7 @@ def run_transform(*args):
         with stdout_replaced(buf):
             schema2html.main(args)
         return buf
-    return schema2html.main(args)
+    return schema2html.main(args) # pragma: no cover
 
 
 
@@ -79,6 +79,9 @@ class TestSchema2HTML(unittest.TestCase):
             res = run_transform(input_file(name))
             self.assertIn('</html>', res.getvalue())
 
+    def test_cover_logging_components(self):
+        res = run_transform('--package', 'ZConfig.components.logger')
+        self.assertIn('eventlog', res.getvalue())
 
 def test_suite():
     return unittest.defaultTestLoader.loadTestsFromName(__name__)

pip install -e .[dev]
parse_log_pytest
pytest
pytest coverage zope.testrunner nti.sphinxcontrib-programoutput
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
Babel==2.11.0
certifi==2021.5.30
charset-normalizer==2.0.12
coverage==6.2
docutils==0.18.1
idna==3.10
imagesize==1.4.1
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
Jinja2==3.0.3
MarkupSafe==2.0.1
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
nti.sphinxcontrib-programoutput==0.10
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
Pygments==2.14.0
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
pytz==2025.2
requests==2.27.1
six==1.17.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-programoutput==0.17
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
urllib3==1.26.20
-e git+https://github.com/zopefoundation/ZConfig.git@a79e6a49c983eba104aff4e0869002437a66426f#egg=ZConfig
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work
zope.exceptions==4.6
zope.interface==5.5.2
zope.testrunner==5.6

name: ZConfig
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - babel==2.11.0
      - charset-normalizer==2.0.12
      - coverage==6.2
      - docutils==0.18.1
      - idna==3.10
      - imagesize==1.4.1
      - jinja2==3.0.3
      - markupsafe==2.0.1
      - nti-sphinxcontrib-programoutput==0.10
      - pygments==2.14.0
      - pytz==2025.2
      - requests==2.27.1
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-programoutput==0.17
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - urllib3==1.26.20
      - zope-exceptions==4.6
      - zope-interface==5.5.2
      - zope-testrunner==5.6
prefix: /opt/conda/envs/ZConfig

ZConfig/tests/test_schema2html.py::TestSchema2HTML::test_cover_logging_components
ZConfig/tests/test_schema2html.py::TestSchema2HTML::test_cover_all_schemas ZConfig/tests/test_schema2html.py::TestSchema2HTML::test_no_schema ZConfig/tests/test_schema2html.py::TestSchema2HTML::test_schema_only ZConfig/tests/test_schema2html.py::TestSchema2HTML::test_schema_only_redirect ZConfig/tests/test_schema2html.py::test_suite
ZConfig/info.py ZConfig/schema2html.py
ZConfig/info.py ZConfig/schema2html.py