Logging Extension & Configuration
Custom logging components can easily be written to integrate with other tracing and logging systems, and applied either to specific documents, or globally to all document generation activities.
TODO: This needs to be updated from the configuration and logging processor
How It Works
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Template XML β
β <custom:StatCard ... /> β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β XML Namespace β
β xmlns:custom='http://mycompany.com/components' β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Namespace Registration (scrybersettings.json) β
β XMLNamespace: http://mycompany.com/components β
β AssemblyPrefix: MyCompany.Components, MyAssembly β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Type Resolution β
β Load assembly β Reflect types β Match "StatCard" β
β Found: MyCompany.Components.StatCard β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Component Instantiation β
β Activator.CreateInstance(typeof(StatCard)) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Configuration
JSON Configuration
scrybersettings.json:
{
"Scryber": {
"Parsing": {
"Namespaces": [
{
"XMLNamespace": "http://mycompany.com/schemas/components",
"AssemblyPrefix": "MyCompany.Components, MyCompany.Components"
},
{
"XMLNamespace": "http://mycompany.com/schemas/charts",
"AssemblyPrefix": "MyCompany.Charts, MyCompany.Charts"
}
]
}
}
}
Configuration Properties
| Property | Description | Example |
|---|---|---|
XMLNamespace |
XML namespace URI (must match xmlns:prefix declaration) |
http://mycompany.com/components |
AssemblyPrefix |
Assembly-qualified namespace: Namespace, AssemblyName |
MyCompany.Components, MyCompany.Components |
Template Declaration
Once registered, use the namespace in templates:
<?xml version='1.0' encoding='utf-8' ?>
<html xmlns='http://www.w3.org/1999/xhtml'
xmlns:custom='http://mycompany.com/schemas/components'
xmlns:charts='http://mycompany.com/schemas/charts'>
<body>
<main>
<!-- Resolves to MyCompany.Components.StatCard -->
<custom:StatCard value='100' label='Sales' />
<!-- Resolves to MyCompany.Charts.BarChart -->
<charts:BarChart data='{@:ChartData}' />
</main>
</body>
</html>
Type Resolution Process
1. Assembly Loading
When the parser encounters an unknown element (e.g., <custom:StatCard>), it:
- Extracts the namespace prefix (
custom) - Looks up the XML namespace URI (
http://mycompany.com/schemas/components) - Finds the AssemblyPrefix (
MyCompany.Components, MyCompany.Components) - Loads or locates the assembly (
MyCompany.Components.dll)
2. Type Reflection
Implementation in ParserDefintionFactory.UnsafeGetType():
private static Type UnsafeGetType(string xmlnamespace, string name,
ParsingOptions options, bool throwNotFound, out NamespaceKey key)
{
// Check cache first
string fullkey = xmlnamespace + ":" + name;
if (_knownTypes.TryGetValue(fullkey, out Type found))
{
key = default;
return found;
}
// Look up assembly for XML namespace
string assemblyPrefixString =
options.LookupAssemblyForXmlNamespace(xmlnamespace, throwNotFound);
if (string.IsNullOrEmpty(assemblyPrefixString))
return null;
// Parse assembly-qualified name
string[] parts = assemblyPrefixString.Split(',');
string namespacePrefix = parts[0].Trim();
string assemblyName = parts.Length > 1 ? parts[1].Trim() : null;
// Load assembly
Assembly assembly = null;
if (!string.IsNullOrEmpty(assemblyName))
{
assembly = Assembly.Load(assemblyName);
}
// Build full type name
string fullTypeName = namespacePrefix + "." + name;
// Try to get type
Type type = null;
if (assembly != null)
{
type = assembly.GetType(fullTypeName, false);
}
else
{
type = Type.GetType(fullTypeName, false);
}
// Cache result
if (type != null)
{
_knownTypes[fullkey] = type;
}
return type;
}
3. Type Caching
Resolved types are cached in _knownTypes dictionary to avoid repeated reflection:
private static Dictionary<string, Type> _knownTypes =
new Dictionary<string, Type>();
Complete Example
1. Component Assembly
MyCompany.Components/StatCard.cs:
using Scryber;
using Scryber.Components;
namespace MyCompany.Components
{
[PDFParsableComponent("StatCard")]
public class StatCard : Panel
{
[PDFAttribute("value")]
public string Value { get; set; }
[PDFAttribute("label")]
public string Label { get; set; }
public StatCard() : base(ObjectTypes.Panel)
{
}
protected override void OnInit(InitContext context)
{
base.OnInit(context);
BuildContent();
}
private void BuildContent()
{
var valueLabel = new Label { Text = Value };
valueLabel.Style.Font.FontSize = 24;
this.Contents.Add(valueLabel);
var labelText = new Label { Text = Label };
labelText.Style.Font.FontSize = 12;
this.Contents.Add(labelText);
}
}
}
2. Configuration
scrybersettings.json:
{
"Scryber": {
"Parsing": {
"Namespaces": [
{
"XMLNamespace": "http://mycompany.com/schemas/components",
"AssemblyPrefix": "MyCompany.Components, MyCompany.Components"
}
]
}
}
}
3. Template
Dashboard.pdfx:
<?xml version='1.0' encoding='utf-8' ?>
<html xmlns='http://www.w3.org/1999/xhtml'
xmlns:custom='http://mycompany.com/schemas/components'>
<body>
<main>
<custom:StatCard value='$125,432' label='Total Sales' />
<custom:StatCard value='1,847' label='Active Users' />
</main>
</body>
</html>
4. Application
Program.cs:
using Microsoft.Extensions.DependencyInjection;
using Scryber;
using Scryber.Components;
// Setup dependency injection
var services = new ServiceCollection();
services.AddScryber(); // Loads scrybersettings.json
var provider = services.BuildServiceProvider();
// Parse document with custom components
using (var reader = new StreamReader("Dashboard.pdfx"))
{
var doc = Document.ParseDocument(reader, ParseSourceType.DynamicContent);
doc.ProcessDocument("Dashboard.pdf");
}
Advanced Configuration
Multiple Namespaces
Register multiple component libraries:
{
"Scryber": {
"Parsing": {
"Namespaces": [
{
"XMLNamespace": "http://mycompany.com/components",
"AssemblyPrefix": "MyCompany.Components, MyCompany.Components"
},
{
"XMLNamespace": "http://mycompany.com/charts",
"AssemblyPrefix": "MyCompany.Charting, MyCompany.Charting"
},
{
"XMLNamespace": "http://thirdparty.com/widgets",
"AssemblyPrefix": "ThirdParty.Widgets, ThirdParty.Widgets"
}
]
}
}
}
Nested Namespaces
Components can exist in nested namespaces:
Code structure:
MyCompany.Components
βββ StatCard.cs
βββ Cards
β βββ ProductCard.cs
βββ Charts
βββ BarChart.cs
Configuration:
{
"Scryber": {
"Parsing": {
"Namespaces": [
{
"XMLNamespace": "http://mycompany.com/components",
"AssemblyPrefix": "MyCompany.Components, MyCompany.Components"
},
{
"XMLNamespace": "http://mycompany.com/components/cards",
"AssemblyPrefix": "MyCompany.Components.Cards, MyCompany.Components"
},
{
"XMLNamespace": "http://mycompany.com/components/charts",
"AssemblyPrefix": "MyCompany.Components.Charts, MyCompany.Components"
}
]
}
}
}
Template:
<html xmlns='http://www.w3.org/1999/xhtml'
xmlns:custom='http://mycompany.com/components'
xmlns:cards='http://mycompany.com/components/cards'
xmlns:charts='http://mycompany.com/components/charts'>
<custom:StatCard ... /> <!-- MyCompany.Components.StatCard -->
<cards:ProductCard ... /> <!-- MyCompany.Components.Cards.ProductCard -->
<charts:BarChart ... /> <!-- MyCompany.Components.Charts.BarChart -->
</html>
Implementation Details
Namespace Lookup
ParsingOptions.LookupAssemblyForXmlNamespace():
public string LookupAssemblyForXmlNamespace(string xmlnamespace, bool throwNotFound)
{
NamespaceDefn found = null;
// Search registered namespaces
foreach (NamespaceDefn defn in this.Namespaces)
{
if (string.Equals(defn.XmlNamespace, xmlnamespace,
StringComparison.OrdinalIgnoreCase))
{
found = defn;
break;
}
}
if (found == null)
{
if (throwNotFound)
throw new PDFParserException(
$"No assembly registered for XML namespace: {xmlnamespace}");
return null;
}
return found.AssemblyPrefix;
}
Namespace Discovery
ParserDefintionFactory.PopulateNamespaceFromAssembly():
When an assembly is loaded, Scryber scans for types with [PDFParsableComponent]:
private static void PopulateNamespaceFromAssembly(
string xmlnamespace, Assembly assembly, NamespaceDefn defn)
{
Type[] alltypes = assembly.GetTypes();
foreach (Type atype in alltypes)
{
// Check for PDFParsableComponent attribute
Attribute found = System.Attribute.GetCustomAttribute(
atype, typeof(PDFParsableComponentAttribute), false);
if (found != null)
{
PDFParsableComponentAttribute parseable =
(PDFParsableComponentAttribute)found;
string name = string.IsNullOrEmpty(parseable.Name)
? atype.Name
: parseable.Name;
// Cache type definition
defn.Components[name] = new ComponentTypeDefinition(atype, name);
}
}
}
Best Practices
Namespace URI Design
- Use your company domain:
http://mycompany.com/schemas/... - Include version for breaking changes:
http://mycompany.com/schemas/v2/components - Group related components:
http://mycompany.com/components/charts - Document namespace URIs
Assembly Organization
- Group related components in same assembly
- Use consistent naming:
Company.Product.Components - Version assemblies appropriately
- Document component libraries
Component Discovery
- Mark all components with
[PDFParsableComponent] - Use descriptive element names
- Avoid name collisions across namespaces
- Document component APIs
Performance
- Namespace resolution is cached - registration order doesnβt impact performance
- Assembly loading happens once per namespace
- Type reflection is cached - repeated use is fast
Troubleshooting
βNo assembly registered for XML namespaceβ
- Verify namespace URI matches exactly (including http/https, case)
- Check scrybersettings.json is loaded
- Ensure JSON syntax is valid
- Verify namespace is in
Parsing:Namespaces[]array
βCould not load type βMyCompany.Components.StatCardββ
- Check assembly name matches AssemblyPrefix
- Verify assembly is referenced by application
- Ensure namespace matches AssemblyPrefix
- Check component class is public
- Verify [PDFParsableComponent] attribute is present
Type not found in assembly
- Check class is public
- Verify namespace matches configuration
- Ensure class name matches element name (or [PDFParsableComponent(βNameβ)])
- Check assembly is compiled and up-to-date
Namespace prefix undeclared
- Add
xmlns:prefix='...'to Document root element - Verify URI matches registered namespace exactly
- Check for typos in namespace URI
Related Documentation
- Custom Components - Creating parseable components
- Processing Instructions - Document-level configuration
- Integration Example - Complete namespace registration example
- Best Practices - Namespace design patterns