mirror of
https://gitea.invidious.io/iv-org/shard-ameba.git
synced 2024-08-15 00:53:29 +00:00
136 lines
4.3 KiB
Crystal
136 lines
4.3 KiB
Crystal
class Ameba::Source
|
|
# This class performs the heavy lifting in the source rewriting process.
|
|
# It schedules code updates to be performed in the correct order.
|
|
#
|
|
# For simple cases, the resulting source will be obvious.
|
|
#
|
|
# Examples for more complex cases follow. Assume these examples are acting on
|
|
# the source `puts(:hello, :world)`. The methods `#wrap`, `#remove`, etc.
|
|
# receive a range as the first two arguments; for clarity, examples below use
|
|
# English sentences and a string of raw code instead.
|
|
#
|
|
# ## Overlapping deletions:
|
|
#
|
|
# * remove `:hello, `
|
|
# * remove `, :world`
|
|
#
|
|
# The overlapping ranges are merged and `:hello, :world` will be removed.
|
|
#
|
|
# ## Multiple actions at the same end points:
|
|
#
|
|
# Results will always be independent of the order they were given.
|
|
# Exception: rewriting actions done on exactly the same range (covered next).
|
|
#
|
|
# Example:
|
|
#
|
|
# * replace `, ` by ` => `
|
|
# * wrap `:hello, :world` with `{` and `}`
|
|
# * replace `:world` with `:everybody`
|
|
# * wrap `:world` with `[`, `]`
|
|
#
|
|
# The resulting string will be `puts({:hello => [:everybody]})`
|
|
# and this result is independent of the order the instructions were given in.
|
|
#
|
|
# ## Multiple wraps on same range:
|
|
#
|
|
# * wrap `:hello` with `(` and `)`
|
|
# * wrap `:hello` with `[` and `]`
|
|
#
|
|
# The wraps are combined in order given and results would be `puts([(:hello)], :world)`.
|
|
#
|
|
# ## Multiple replacements on same range:
|
|
#
|
|
# * replace `:hello` by `:hi`, then
|
|
# * replace `:hello` by `:hey`
|
|
#
|
|
# The replacements are made in the order given, so the latter replacement
|
|
# supersedes the former and `:hello` will be replaced by `:hey`.
|
|
#
|
|
# ## Swallowed insertions:
|
|
#
|
|
# * wrap `world` by `__`, `__`
|
|
# * replace `:hello, :world` with `:hi`
|
|
#
|
|
# A containing replacement will swallow the contained rewriting actions
|
|
# and `:hello, :world` will be replaced by `:hi`.
|
|
#
|
|
# ## Implementation
|
|
#
|
|
# The updates are organized in a tree, according to the ranges they act on
|
|
# (where children are strictly contained by their parent).
|
|
class Rewriter
|
|
getter code : String
|
|
|
|
def initialize(@code)
|
|
@action_root = Rewriter::Action.new(0, code.size)
|
|
end
|
|
|
|
# Returns `true` if no (non trivial) update has been recorded
|
|
def empty?
|
|
@action_root.empty?
|
|
end
|
|
|
|
# Replaces the code of the given range with *content*.
|
|
def replace(begin_pos, end_pos, content)
|
|
combine begin_pos, end_pos,
|
|
replacement: content.to_s
|
|
end
|
|
|
|
# Inserts the given strings before and after the given range.
|
|
def wrap(begin_pos, end_pos, insert_before, insert_after)
|
|
combine begin_pos, end_pos,
|
|
insert_before: insert_before.to_s,
|
|
insert_after: insert_after.to_s
|
|
end
|
|
|
|
# Shortcut for `replace(begin_pos, end_pos, "")`
|
|
def remove(begin_pos, end_pos)
|
|
replace(begin_pos, end_pos, "")
|
|
end
|
|
|
|
# Shortcut for `wrap(begin_pos, end_pos, content, nil)`
|
|
def insert_before(begin_pos, end_pos, content)
|
|
wrap(begin_pos, end_pos, content, nil)
|
|
end
|
|
|
|
# Shortcut for `wrap(begin_pos, end_pos, nil, content)`
|
|
def insert_after(begin_pos, end_pos, content)
|
|
wrap(begin_pos, end_pos, nil, content)
|
|
end
|
|
|
|
# Shortcut for `insert_before(pos, pos, content)`
|
|
def insert_before(pos, content)
|
|
insert_before(pos, pos, content)
|
|
end
|
|
|
|
# Shortcut for `insert_after(pos, pos, content)`
|
|
def insert_after(pos, content)
|
|
insert_after(pos, pos, content)
|
|
end
|
|
|
|
# Applies all scheduled changes and returns modified source as a new string.
|
|
def process
|
|
String.build do |io|
|
|
last_end = 0
|
|
@action_root.ordered_replacements.each do |begin_pos, end_pos, replacement|
|
|
io << code[last_end...begin_pos] << replacement
|
|
last_end = end_pos
|
|
end
|
|
io << code[last_end...code.size]
|
|
end
|
|
end
|
|
|
|
protected def combine(begin_pos, end_pos, **attributes)
|
|
check_range_validity(begin_pos, end_pos)
|
|
action = Rewriter::Action.new(begin_pos, end_pos, **attributes)
|
|
@action_root = @action_root.combine(action)
|
|
end
|
|
|
|
private def check_range_validity(begin_pos, end_pos)
|
|
return unless begin_pos < 0 || end_pos > code.size
|
|
raise IndexError.new(
|
|
"The range #{begin_pos}...#{end_pos} is outside the bounds of the source"
|
|
)
|
|
end
|
|
end
|
|
end
|