← Back to list

Architectural Analysis of Jaroslava

In the last weeks I developed a markup language, named Jaroslava (pronounced “yah-ruh-SLAH-vah”), starting with creating an easy way to…

Victor L. · 2026-07-04 15:36 · 10 claps · 10.3 min read
#programming-languages #markup-language #software-architecture #microkernel-pattern #software-development
Open on Medium ↗
Wiki topics: 💻 · Programming 🏛️ · Architecture

Architectural Analysis of Jaroslava

In the last weeks I developed a markup language, named Jaroslava (pronounced “yah-ruh-SLAH-vah”), starting with creating an easy way to create interface without directly using HTML. Similar markdown, however more powerful. I sought simplicity to achieve a user-friendly design. Furthermore, standardization templates have been created to make things even easier (such as link hub, portfolio, blog, and more). Today, I will describe its software architecture, the decisions made, its structure, and how to create a plugin.

Before starting, I want to provide some context software architecture concepts and the chosen architecture, before diving into the implementation details.

Basic concepts of software architecture

The architecture of a software system includes elements that deal with: functionality or behavior, information or data, and interaction. It can be from a single operation a entire system. To deal with this, we have two elements:

  • Component: is a modular, replaceable and encapsulated unit that represents a functional part of the system. This element encapsulate a subset of functionality and/or data in a system’s architecture. At the compilation level, it can be compiled in isolation.
  • Connector: for communication between components, we have the connector. The simplest and most widely used type of connector is procedure call. Interaction, depending on the system, can be critical, which is why connectors have a wide classification and variation — perhaps it doesn’t make sense to go into this in depth here.

A Component (Filter) and Connector (Pipe) as a UML Class (Object).

A Component (Filter) and Connector (Pipe) as a UML Class (Object).

Architectural Project

This project should be an extension to news markings. For this reason, I chose the Plugin Architecture Pattern. It separates a minimal functional core, in which, in this scenario, it is used to manage the plugins, from extended functionality and customer-specific parts (rendering HTML).

No components are hardcoded; each plugin is flexible regarding registration and removal, making the addition of a new plugin very simple — confining the “complexity” to the plugin’s creation rather than its integration into the project — and, most importantly, ensuring minimal interference with other, already stable components.

Architectural Analogy

Architectural Analogy

The Jaroslava SDK is the interface responsible for converting .jaro source code into an internal representation (AST) and, based on that AST, performing validation, reconstructing the source code (serialization), and rendering it to HTML.

The AST serves as the ecosystem’s central contract. Every editor, preview tool, exporter, and renderer is built upon the AST rather than directly on the .jaro source text. This design choice avoids a direct .jaro to HTML link, creating an abstraction intended to facilitate future integrations.

Responsibility of Each Package

Currently, there are 9 packages in the project, separated by responsibility to ensure modularity.

packages/
├── types
├── utils                    
├── core
├── parser
├── serializer
├── validator
├── renderer-html
├── plugin-core-components
└── sdk

@jaroslava/types

Dependencies: no dependencies

Defines the AST node shapes (DocumentNode, ComponentNode, TextNode, LinkNode, CallNode, CodeBlockNode, InlineGroupNode), the JaroAst root, Diagnostic, and the plugin contract (ComponentDefinition, JaroslavaPlugin, hooks). This package has no runtime logic and no dependencies on any other package in the SDK; everything else depends on it.

@jaroslava/utils

Dependencies: **@jaroslava/types**

Small, pure, framework-agnostic helpers used by multiple packages: AST traversal (walk, walkAst, findAll, findById, pathFor), id generation, indentation/quoting string helpers, and the shared inline-expression parser (parseInline, handling -> links, name(args) calls, and +-joined groups). This lives separately from @jaroslava/core because it has zero plugin/registry awareness — it's pure data transformation, usable even by a tool that never touches the registry

@jaroslava/core

Dependencies: **@jaroslava/types , @jaroslava/utils**

Has zero knowledge of any specific component kind; it only knows how to register, look up, and dispatch to whatever plugins are installed. These are their main components:

  • **PluginRegistry** : single source of truth about which components exist. Knows how to register, query (getComponent), list and uninstall components; detects duplicate records (DuplicateComponentError).
  • **JaroslavaContext** : packages a PluginRegistrywith the installation of a set of plugins, so that the application does not need to manage this manually for each call.

@jaroslava/parser

Dependencies: **@jaroslava/types , @jaroslava/utils, @jaroslava/core**

Converts .jaro source text into a JaroAst. Before understanding the parser, it is necessary to understand what it needs to produce. The Jaroslava AST consists of seven node types, all sharing a BaseNodewith an id, type, and span(location in the source code).

BaseNode
├── DocumentNode    — root of a page (@page). Contains: kind, attrs, children[]
├── ComponentNode   — any @kind block. Contains: kind, attrs, children[]
├── TextNode        — prose/free text. Contains: value (string)
├── LinkNode        — link arrow (->). Contains: label?, href, internal (bool)
├── CallNode        — img() call expression. Contains: callee, args[]
├── CodeBlockNode   — verbatim code block. Contains: code
└── InlineGroupNode — inline sequence. Contains: items[]

In the first phase, the input is a raw string; lines are then separated, and blank lines are discarded. This is done because Jaroslava uses indentation, rather than markers, to delimit blocks. The opening or closing level of a block is determined by the change in depth between the current line and the next non-empty line. After separating the lines, the number of leading spaces or tabs is calculated for each; this value serves as the depth used to determine the levels of each component and its children. It follows the same INDENT/DEDENT style as the Python tokenizer.

Line 1: "@page"       → width=0    stack=[0]       depth=0
Line 2: " title: X"   → width=2    stack=[0,2]     depth=1
Line 3: " @list"      → width=2    stack=[0,2]     depth=1
Line 4: " direction"  → width=4    stack=[0,2,4]   depth=2
Line 5: "@profile"    → width=0    stack=[0]       depth=0

In the next phase, the DocumentNode[] is created. The builder interprets any line with a depth of 0 as the start of a component. If it is an @page, a DocumentNode is created; otherwise, the component is added as a child to the DocumentNode. For each component, the recursive function buildComponentSubtree(lines, startIndex, ctx) is called to construct the component and any potential children. Processing of a component’s body stops when line.depth < bodydepth, given that line.depth < bodydepth = header.depth + 1. For each line in the body, the builder checks five conditions in order:

  • Remainder: captures the remainder of the component header. If the plugin implements parseInlineHeader, this is expected to be returned as a key-value pair and subsequently added as an attribute.
  • Another component: if a line within the component — i.e., within its body — starts with @, recursion is triggered, and this new component is added as a child (node.children). — Item line: checks for grouped items, such as Label -> "url", img("link.com"), and a + b + c.
  • Component attribute: uses regex to check if the line contains an attribute consisting of a key and a value; if found, it adds the attribute to the node: node.attrs[key]=coerceAttrValue(value).
  • Default: handles anything that did not meet the previous conditions; it can return any node type (Text ,Link ,Call, or InlineGroup), which is then added to the parent node as a child.

There are special cases for the parser. A link with an href starting with id: — such as id:started — produces a LinkNode with internal:true. The HTML renderer interprets this as an anchor (started) rather than an external URL. This distinction exists within the AST (not just the renderer) so that other consumers can handle the two cases differently without needing to parse URL strings.

Unknown components are never dropped — they become generic ComponentNode with a warning diagnosis (unknown-component).

@jaroslava/serializer

Dependencies: **@jaroslava/types , @jaroslava/utils, @jaroslava/core**

The inverse of the parser: JaroAst -> .jaro source text. Generic components serialize via a default key: value + indented-children algorithm; components with custom syntax override this via the plugin's serialize hook. Output is always standardized/formatted, the serializer does not attempt byte-for-byte preservation of the original source's whitespace/comments. The serializer has two modules:

  • **component.ts** — serializes DocumentNodeand ComponentNode(the @kind header, attributes, and children recursively)
  • **primitive.ts**— serializes inline nodes (Text ,Link ,Call, or InlineGroup, CodeBlock ) back into their textual form

The public entry point (serialize) maps each DocumentNodefrom ast.documentsto a text block and separates the blocks with a blank line. The serializer does not attempt to reproduce the original text; instead, it produces a normalized version, as there is no reference to the original text — only the abstract syntax tree. Each plugin can implement a specific serializer using serialize, but if one is not provided, the generic serializer handles the task. Child nodes are serialized recursively. Serialization is performed for attributes, values, and inline nodes (TextNode ,LinkNode ,CallNode, InlineGroupNode andCodeBlockNode).

@jaroslava/validator

Dependencies: **@jaroslava/types , @jaroslava/utils, @jaroslava/core**

Validates an AST and returns the result as a Diagnostic[]. It operates in three layers:

  1. Generic schema validation: checking for mandatory attributes, attribute types, enum membership, and allowed child types/kinds — all driven by the ComponentSchema object declared by the plugin itself. The validator never handles specific plugins as special cases; it simply reads the schema registered for that kind.
  2. Per-component validation hooks (validate): for contextual rules that a static schema does not capture.
  3. Global validators (globalValidators): for whole-document invariants not tied to a single node (e.g., “every @page requires a unique titlewithin the site”).

@jaroslava/renderer-html

Dependencies: **@jaroslava/types , @jaroslava/utils, @jaroslava/core**

Converts an AST into HTML + CSS. Rendering is done recursively through the renderof each plugin, with the HTML of the children ready. CSS and <head> fragments returned by every component used on a page are deduplicated and merged. Components with no registered plugin (or a plugin with no render hook) render a generic, inert <div class="jaro-unknown-component" data-kind="..."> wrapper instead of throwing — a missing plugin degrades gracefully rather than crashing a page in production.

It renders the structural node types that are NOT components — and therefore lack a plugin to which execution can be delegated — known as primitives (text, link, img, codeblock, and Inline Group).

@jaroslava/plugin-core-components

Dependencies: **@jaroslava/types , @jaroslava/utils**

It delivers the “standard” set of components, implementing a common interface: the JaroslavaPlugin. A plugin can provide components, generic validators, and/or a fully custom rendering target (not just HTML). They are explicitly installed by the application (Jaroslava.create({ plugins: [coreComponentsPlugin] })), exactly as any third-party plugin would be.

@jaroslava/sdk

Dependencies: all previous packages

A convenience facade: the Jaroslava class wraps one JaroslavaContext and exposes parse(), serialize(), validate(), renderHtml(). It also re-exports the low-level functions from all packages, for those who prefer to manage their own registry/context explicitly.

System Diagram

System Diagram

Data Flow

The SDK is not a single, linear pipeline. Once the AST is built, it can follow three independent paths (or any combination thereof), all starting from the same point (the AST) and never interfering with one another.

Jaro Processing Pipeline

Jaro Processing Pipeline

None of these pipelines alter the original AST — they are all pure read operations. The AST is immutable from the perspective of pipelines (in the functional sense; a package never modifies the AST it receives, but only reads from it).

Once the .jaro text is available, the first step is parsing to build the AST; this is the only stage that creates the AST, while subsequent stages merely consume it. With the AST created, we can proceed to the quality assurance package, which generates validation diagnostics. Next, we can render the AST to HTML — a process performed recursively, rendering children before the parent. Finally — though this is optional — we can reconstruct the .jaro format, where the serializer traverses the AST recursively, assembling an array of text lines for each node.

Concrete example

Gross input (5 lines):

@page
  title: "Home"

@hero
  heading: Welcome

After lexer:

[
  { lineNumber: 1, content: "@page", depth: 0, indentWidthSpaces: 0 },
  { lineNumber: 2, content: "title: \"Home\"", depth: 1, indentWidthSpaces: 2 },
  { lineNumber: 4, content: "@hero", depth: 0, indentWidthSpaces: 0 },
  { lineNumber: 5, content: "heading: Welcome", depth: 1, indentWidthSpaces: 2 },
]
// Note: line 3 (empty) was discarded

After parser (JaroAst):

{
  documents: [
    {
      id: "n_1",
      type: "Document",
      kind: "page",
      version: "1.0.0",
      attrs: { title: "Home" },
      children: [
        {
          id: "n_2",
          type: "Component",
          kind: "hero",
          version: "1.0.0",
          attrs: { heading: "Welcome" },
          children: []
        }
      ],
      span: { startLine: 1, startColumn: 1, endLine: 5, endColumn: 18 }
    }
  ],
  diagnostics: []
}

After validator:

{
  ok: true,
  value: true,
  diagnostics: [
    // (nenhum erro; @page e @hero têm schemas válidos)
  ]
}

After renderer-html:

{
  pages: [
    {
      html: `<!doctype html>
<html lang="en" data-theme="light">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Home</title>
  <style>...base css...</style>
</head>
<body class="jaro-page jaro-layout-default">
  <section class="jaro-hero">
    <h2 class="jaro-hero-heading">Welcome</h2>
    <div class="jaro-hero-body"></div>
  </section>
</body>
</html>`,
      css: "...css deduped..." (se cssOutput: "separate")
    }
  ]
}

Creating a new plugin

There are no hardcoded components anywhere in any package. Every component are implemented in plugin-core-components using the exact same ComponentDefinitioncontract available to any third party.

The plugin contract (from @jaroslava/types)

The plugin contract (from @jaroslava/types)

In example above, where an @testimonial component — wholly unknown to the rest of the SDK — is registered and rendered with zero special-casing.

import { Jaroslava } from "@jaroslava/sdk";
import type {JaroslavaPlugin} from "@jaroslava/types";

const testimonialPlugin: JaroslavaPlugin = {
  name: "@acme/jaroslava-plugin-testimonial",
  components: [
    {
      kind: "testimonial",
      displayName: "Testimonial",
      schema: {
        kind: "testimonial",
        attributes: {
          author: { type: "string", required: true },
          quote: { type: "string", required: true },
        },
      },
      render(node) {
        return {
          html: "
            <blockquote class="acme-testimonial">
              "${node.attrs.quote}" - <cite>${node.attrs.author}</cite>
            </blockquote>
          ",
          css: [
            ".acme-testimonial { 
                font-style: italic; 
                border-left: 3px solid #8b5cf6; 
                padding-left: 1rem; 
            }",
          ],
        };
      },
    },
  ],
};

const sdk = await Jaroslava.create({
  plugins: [coreComponentsPlugin, testimonialPlugin],
});

const source = `@page
  title: "Customer Stories"

@testimonial
  author: "Maria"
  quote: "This product changed everything."
`;

const { value: ast } = sdk.parse(source);
// do the rest

Without the plugin installed, the same source still parses successfully (the original principle: the AST must never require the .jaro source again) and renders a generic, debuggable placeholder instead of crashing.

It’s important to note that, even without the plugin installed, the same source code is still successfully analyzed and a generic placeholder is rendered instead of the system crashing.

Conclusion

It is a promising project, though there is still much to be done. Simplification aimed at accessibility is what drove me to create this project. You, my dear reader, are more than welcome to help this project grow. Feel free to open a pull request or offer constructive criticism. I look forward to everyone’s participation. See you later!👋

Checkout the result:

Jaroslava.app

Jaroslava.sdk

References

R. N. Taylor, N. Medvidovic, and E. M. Dashofy, Software Architecture: Foundations, Theory, and Practice, 1st ed. Hoboken, NJ, USA: Wiley, 2009.

F. Buschmann, R. Meunier, H. Rohnert, P. Sommerlad, and M. Stal, Pattern-Oriented Software Architecture: A System of Patterns, vol. 1, 1st ed. Chichester, UK: Wiley, 1996.

J. Ivers, P. Clements, D. Garlan, R. Nord, B. Schmerl, and J. R. Oviedo Silva, Documenting Component and Connector Views with UML 2.0, Software Engineering Institute, Carnegie Mellon University, Apr. 2004.


메타데이터
post_id
5ef0d8bc8f67
slug
architectural-analysis-of-jaroslava-5ef0d8bc8f67
url
https://medium.com/@Victorldev/architectural-analysis-of-jaroslava-5ef0d8bc8f67
canonical_url
https://medium.com/@Victorldev/architectural-analysis-of-jaroslava-5ef0d8bc8f67
author_url
https://medium.com/@Victorldev
status
ok
fetched_at
2026-08-11 12:41:33