Class: Markbridge::Normalizer::RuleSet
- Inherits:
-
Object
- Object
- Markbridge::Normalizer::RuleSet
- Defined in:
- lib/markbridge/normalizer/rule_set.rb
Overview
Maps a node and its ancestor stack to a strategy.
Rules are keyed by Class on both sides, and matching follows the
class ancestry: a rule registered for a base class also applies to
its subclasses, for the parent and for the child. Only classes
inside the AST::Node hierarchy take part; the walk never reaches
Object, so a rule keyed on a class outside the AST never matches.
Precedence, in this order:
- Stack position — the outermost ancestor in the stack with any matching rule wins.
- Child class specificity — at that ancestor, a rule keyed on the node's own class beats a rule keyed on a superclass.
- Parent class specificity — then a rule keyed on the ancestor's own class beats a rule keyed on a superclass.
A rule registered for the exact (parent, child) pair therefore
always overrides an inherited one. Registering a rule for a pair
that already has one replaces it, so later layers (Discourse, a
consumer's #rule) override earlier ones.
Constant Summary collapse
- NO_MATCH =
[nil, nil].freeze
- EMPTY_CANDIDATES =
Shared "no rule targets this class" marker, so the cache entry for a rule-less class costs no allocation.
[].freeze
Instance Method Summary collapse
-
#add(parent:, child:, strategy:) ⇒ self
Register (or replace) a rule.
-
#freeze ⇒ Object
Freeze so a shared instance raises if something tries to change it.
-
#initialize ⇒ RuleSet
constructor
A new instance of RuleSet.
-
#resolve(child, ancestors, walk_cache) ⇒ Array(Object, AST::Element), Array(nil, nil)
Resolve the strategy for
childgiven its ancestor stack (root first).
Constructor Details
#initialize ⇒ RuleSet
Returns a new instance of RuleSet.
32 33 34 35 36 |
# File 'lib/markbridge/normalizer/rule_set.rb', line 32 def initialize @by_parent = {} # parent_class => { child_class => strategy } @child_classes = Set.new # @candidates_cache stays unset (nil) until #freeze fills it. end |
Instance Method Details
#add(parent:, child:, strategy:) ⇒ self
Register (or replace) a rule.
44 45 46 47 48 49 |
# File 'lib/markbridge/normalizer/rule_set.rb', line 44 def add(parent:, child:, strategy:) validate_strategy!(strategy) (@by_parent[parent] ||= {})[child] = strategy @child_classes << child self end |
#freeze ⇒ Object
Freeze so a shared instance raises if something tries to change it.
Freezing @by_parent and its inner hashes is enough: #add writes
there before it touches @child_classes, so a frozen instance raises
on the @by_parent write first. @child_classes is never reached, so
it does not need freezing.
Freezing also precomputes the child-rule candidates for every AST
class known at this point (+AST::Node.descendants+ is boot-time
state), so #resolve on a frozen rule set answers with one Hash
lookup instead of going through the per-walk cache. The rules can
not change anymore, so the cache can never go stale; classes
defined after the freeze fall back to the walk cache. The ||= keeps a
second freeze from writing to the then-frozen instance. Like
@child_classes, the cache Hash stays unfrozen — nothing writes
to it after this point.
99 100 101 102 103 104 105 |
# File 'lib/markbridge/normalizer/rule_set.rb', line 99 def freeze @candidates_cache ||= AST::Node.descendants.to_h { |klass| [klass, child_candidates(klass)] } @by_parent.each_value(&:freeze) @by_parent.freeze super end |
#resolve(child, ancestors, walk_cache) ⇒ Array(Object, AST::Element), Array(nil, nil)
Resolve the strategy for child given its ancestor stack (root
first). Returns [strategy, boundary] where boundary is the
outermost ancestor with a rule that matches +child+'s class, or
NO_MATCH (+[nil, nil]+) when nothing matches.
walk_cache caches the child-rule candidates per class. The caller owns
it and should reuse one Hash for a whole tree walk, so each
distinct class is analyzed at most once per walk. Keeping the
cache outside the RuleSet leaves a frozen shared instance free of
per-call state. The cache read is a single Hash lookup because this
method runs for every node in the tree.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/markbridge/normalizer/rule_set.rb', line 68 def resolve(child, ancestors, walk_cache) klass = child.class # A frozen rule set carries a prebuilt candidates cache (see # #freeze); the walk cache is only needed for classes defined after the # freeze and for mutable rule sets. frozen_cache = @candidates_cache candidates = (frozen_cache && frozen_cache[klass]) || (walk_cache[klass] ||= child_candidates(klass)) # Skip the ancestor scan for a class no rule targets (most nodes, for # example plain text). The scan below returns the same result for such # a class, so this only saves work. return NO_MATCH if candidates.empty? scan_ancestors(candidates, ancestors) end |