uniword 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/lib/uniword/cli/main.rb +134 -0
- data/lib/uniword/cli/review_cli.rb +34 -0
- data/lib/uniword/configuration.rb +17 -0
- data/lib/uniword/docx/deterministic_output.rb +47 -0
- data/lib/uniword/docx.rb +1 -0
- data/lib/uniword/find_replace/body_scope.rb +23 -0
- data/lib/uniword/find_replace/comment_scope.rb +23 -0
- data/lib/uniword/find_replace/endnote_scope.rb +23 -0
- data/lib/uniword/find_replace/engine.rb +84 -0
- data/lib/uniword/find_replace/footer_scope.rb +24 -0
- data/lib/uniword/find_replace/footnote_scope.rb +23 -0
- data/lib/uniword/find_replace/header_scope.rb +24 -0
- data/lib/uniword/find_replace/matcher.rb +23 -0
- data/lib/uniword/find_replace/paragraph_walker.rb +61 -0
- data/lib/uniword/find_replace/regex_matcher.rb +38 -0
- data/lib/uniword/find_replace/result.rb +54 -0
- data/lib/uniword/find_replace/scope.rb +98 -0
- data/lib/uniword/find_replace/string_matcher.rb +72 -0
- data/lib/uniword/find_replace/styles_scope.rb +38 -0
- data/lib/uniword/find_replace.rb +29 -0
- data/lib/uniword/infrastructure/zip_packager.rb +49 -3
- data/lib/uniword/lint/builtin_rules/banned_words.rb +35 -0
- data/lib/uniword/lint/builtin_rules/max_paragraph_length.rb +32 -0
- data/lib/uniword/lint/builtin_rules/require_body.rb +23 -0
- data/lib/uniword/lint/builtin_rules/required_style.rb +31 -0
- data/lib/uniword/lint/builtin_rules.rb +23 -0
- data/lib/uniword/lint/engine.rb +32 -0
- data/lib/uniword/lint/result.rb +59 -0
- data/lib/uniword/lint/rule.rb +77 -0
- data/lib/uniword/lint/ruleset.rb +65 -0
- data/lib/uniword/lint.rb +17 -0
- data/lib/uniword/redact/engine.rb +52 -0
- data/lib/uniword/redact/pattern.rb +23 -0
- data/lib/uniword/redact/pattern_library.rb +60 -0
- data/lib/uniword/redact/result.rb +42 -0
- data/lib/uniword/redact.rb +15 -0
- data/lib/uniword/version.rb +1 -1
- data/lib/uniword/wordprocessingml/document_styling.rb +91 -0
- data/lib/uniword/wordprocessingml/settings.rb +2 -0
- data/lib/uniword/wordprocessingml/track_changes.rb +19 -0
- data/lib/uniword/wordprocessingml.rb +1 -0
- data/lib/uniword.rb +10 -0
- metadata +34 -2
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Lint
|
|
5
|
+
# Abstract base class for a single lint rule. Subclasses implement
|
|
6
|
+
# `#check` (yields findings) and declare `name`, `severity`.
|
|
7
|
+
#
|
|
8
|
+
# Open/closed: new rule type = new subclass + (optionally) a
|
|
9
|
+
# YAML schema extension. Engine unchanged.
|
|
10
|
+
class Rule
|
|
11
|
+
# Registered rule subclasses by type name. Constant on `Rule`
|
|
12
|
+
# itself (not subclasses) so `register` calls always reach the
|
|
13
|
+
# same hash. `Hash.new` (rather than `{}`) avoids the
|
|
14
|
+
# Style/MutableConstant autocorrect that would `.freeze` it and
|
|
15
|
+
# break `register`.
|
|
16
|
+
TYPES = Hash.new
|
|
17
|
+
|
|
18
|
+
attr_reader :name, :severity, :options
|
|
19
|
+
|
|
20
|
+
# @param name [Symbol] rule identifier
|
|
21
|
+
# @param severity [Symbol] :error, :warning, or :info
|
|
22
|
+
# @param options [Hash] rule-specific options
|
|
23
|
+
def initialize(name:, severity: :warning, **options)
|
|
24
|
+
@name = name
|
|
25
|
+
@severity = severity
|
|
26
|
+
@options = options
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Walk the document and yield each finding.
|
|
30
|
+
#
|
|
31
|
+
# @param document [Wordprocessingml::DocumentRoot]
|
|
32
|
+
# @yieldparam finding [Hash] { message:, path:, position: }
|
|
33
|
+
# @return [void]
|
|
34
|
+
def check(document)
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Build a finding hash.
|
|
39
|
+
#
|
|
40
|
+
# @param message [String]
|
|
41
|
+
# @param path [String, nil] document path (paragraph index,
|
|
42
|
+
# style id, etc.)
|
|
43
|
+
# @return [Hash]
|
|
44
|
+
def finding(message:, path: nil)
|
|
45
|
+
{ rule: name, severity: severity, message: message, path: path }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
# Register a subclass under a type name for YAML ruleset
|
|
50
|
+
# lookup. Always writes to `Rule::TYPES` regardless of the
|
|
51
|
+
# calling subclass.
|
|
52
|
+
#
|
|
53
|
+
# @param name [Symbol]
|
|
54
|
+
# @param klass [Class<Rule>]
|
|
55
|
+
# @return [void]
|
|
56
|
+
def register(name, klass)
|
|
57
|
+
Rule::TYPES[name] = klass
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Lookup a registered rule subclass by type name.
|
|
61
|
+
#
|
|
62
|
+
# @param name [Symbol]
|
|
63
|
+
# @return [Class<Rule>, nil]
|
|
64
|
+
def type(name)
|
|
65
|
+
Rule::TYPES[name]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# All registered rule types.
|
|
69
|
+
#
|
|
70
|
+
# @return [Hash{Symbol => Class<Rule>}]
|
|
71
|
+
def types
|
|
72
|
+
Rule::TYPES
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Lint
|
|
7
|
+
# Loads rules from a YAML ruleset or symbol registry. Each rule
|
|
8
|
+
# has a `type` (which `Rule` subclass to instantiate) and
|
|
9
|
+
# rule-specific options.
|
|
10
|
+
#
|
|
11
|
+
# Example YAML:
|
|
12
|
+
#
|
|
13
|
+
# rules:
|
|
14
|
+
# - type: sentence_case_headings
|
|
15
|
+
# severity: warning
|
|
16
|
+
# - type: max_paragraph_length
|
|
17
|
+
# severity: warning
|
|
18
|
+
# max: 200
|
|
19
|
+
# - type: banned_words
|
|
20
|
+
# severity: warning
|
|
21
|
+
# words: [really, very, basically]
|
|
22
|
+
class Ruleset
|
|
23
|
+
# @param rules [Array<Rule>]
|
|
24
|
+
def initialize(rules = [])
|
|
25
|
+
@rules = rules
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Array<Rule>]
|
|
29
|
+
attr_reader :rules
|
|
30
|
+
|
|
31
|
+
# Load a ruleset from a YAML file.
|
|
32
|
+
#
|
|
33
|
+
# @param path [String]
|
|
34
|
+
# @return [Ruleset]
|
|
35
|
+
def self.load(path)
|
|
36
|
+
data = YAML.safe_load_file(path)
|
|
37
|
+
from_hash(data || {})
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Build a ruleset from a parsed YAML hash.
|
|
41
|
+
#
|
|
42
|
+
# @param data [Hash] `{"rules" => [{"type" => ..., ...}, ...]}`
|
|
43
|
+
# @return [Ruleset]
|
|
44
|
+
def self.from_hash(data)
|
|
45
|
+
rule_list = (data["rules"] || []).map { |spec| build_rule(spec) }
|
|
46
|
+
new(rule_list)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Build one rule from a spec hash by type name.
|
|
50
|
+
#
|
|
51
|
+
# @param spec [Hash] `{"type" => "name", "severity" => ...}`
|
|
52
|
+
# @return [Rule]
|
|
53
|
+
def self.build_rule(spec)
|
|
54
|
+
type = spec["type"].to_sym
|
|
55
|
+
klass = Rule.types[type]
|
|
56
|
+
raise ArgumentError, "unknown rule type: #{spec['type']}" unless klass
|
|
57
|
+
|
|
58
|
+
kwargs = spec.transform_keys(&:to_sym)
|
|
59
|
+
kwargs.delete(:type)
|
|
60
|
+
kwargs[:name] = type unless kwargs.key?(:name)
|
|
61
|
+
klass.new(**kwargs)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/uniword/lint.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
# Document lint: enforce content + structure rules beyond OPC
|
|
5
|
+
# validity. Backed by a YAML ruleset; rules are open for extension
|
|
6
|
+
# via `Lint::Rule.register`.
|
|
7
|
+
module Lint
|
|
8
|
+
autoload :Engine, "#{__dir__}/lint/engine"
|
|
9
|
+
autoload :Ruleset, "#{__dir__}/lint/ruleset"
|
|
10
|
+
autoload :Rule, "#{__dir__}/lint/rule"
|
|
11
|
+
autoload :Result, "#{__dir__}/lint/result"
|
|
12
|
+
autoload :BuiltinRules, "#{__dir__}/lint/builtin_rules"
|
|
13
|
+
|
|
14
|
+
# Force-load the builtins so their `register` calls run.
|
|
15
|
+
BuiltinRules.to_s
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Redact
|
|
5
|
+
# Redaction orchestrator. Runs each pattern through the
|
|
6
|
+
# FindReplace engine and aggregates per-pattern counts.
|
|
7
|
+
#
|
|
8
|
+
# Open/closed: adding a new pattern = `PatternLibrary.register`
|
|
9
|
+
# call. Engine unchanged.
|
|
10
|
+
class Engine
|
|
11
|
+
# @param document [Wordprocessingml::DocumentRoot]
|
|
12
|
+
# @param patterns [Array<Pattern>, :pii] patterns to apply
|
|
13
|
+
# (`:pii` selects the default library)
|
|
14
|
+
# @param scope [Symbol, Array<Symbol>, :all] find-replace scope
|
|
15
|
+
def initialize(document:, patterns: :pii, scope: :all)
|
|
16
|
+
@document = document
|
|
17
|
+
@patterns = resolve_patterns(patterns)
|
|
18
|
+
@scope = scope
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Redact::Result]
|
|
22
|
+
def run
|
|
23
|
+
result = Result.new
|
|
24
|
+
return result if @patterns.empty?
|
|
25
|
+
|
|
26
|
+
@patterns.each do |pattern|
|
|
27
|
+
matcher = FindReplace::RegexMatcher.new(pattern: pattern.regex,
|
|
28
|
+
replacement: pattern.replacement)
|
|
29
|
+
fr = FindReplace::Engine.new(document: @document,
|
|
30
|
+
matcher: matcher,
|
|
31
|
+
scopes: @scope).run
|
|
32
|
+
result.add(pattern.name, fr.count)
|
|
33
|
+
end
|
|
34
|
+
result
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def resolve_patterns(patterns)
|
|
40
|
+
return PatternLibrary.all if patterns == :pii
|
|
41
|
+
return [] if patterns.nil? || patterns.empty?
|
|
42
|
+
|
|
43
|
+
list = Array(patterns)
|
|
44
|
+
if list.all?(Symbol)
|
|
45
|
+
PatternLibrary.select(list)
|
|
46
|
+
else
|
|
47
|
+
list
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Redact
|
|
5
|
+
# One redaction pattern: a regex plus a label that classifies the
|
|
6
|
+
# kind of PII it matches.
|
|
7
|
+
class Pattern
|
|
8
|
+
attr_reader :name, :regex, :replacement, :description
|
|
9
|
+
|
|
10
|
+
# @param name [Symbol] pattern identifier (e.g. :ssn, :email)
|
|
11
|
+
# @param regex [Regexp] what to match
|
|
12
|
+
# @param replacement [String] replacement text (default "[REDACTED]")
|
|
13
|
+
# @param description [String] human-readable pattern description
|
|
14
|
+
def initialize(name:, regex:, replacement: "[REDACTED]",
|
|
15
|
+
description: nil)
|
|
16
|
+
@name = name
|
|
17
|
+
@regex = regex
|
|
18
|
+
@replacement = replacement
|
|
19
|
+
@description = description
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Redact
|
|
5
|
+
# Built-in library of common PII patterns. Open/closed: callers
|
|
6
|
+
# can register custom patterns via `PatternLibrary.register`.
|
|
7
|
+
class PatternLibrary
|
|
8
|
+
DEFAULT_PATTERNS = [
|
|
9
|
+
# US Social Security Number: AAA-GG-SSSS (no all-zeros groups)
|
|
10
|
+
Pattern.new(name: :ssn,
|
|
11
|
+
regex: /\b(?!000|666|9\d{2})\d{3}-(?!00)\d{2}-(?!0000)\d{4}\b/,
|
|
12
|
+
description: "US Social Security Number"),
|
|
13
|
+
# Email address
|
|
14
|
+
Pattern.new(name: :email,
|
|
15
|
+
regex: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i,
|
|
16
|
+
description: "Email address"),
|
|
17
|
+
# US phone: (555) 555-5555 or 555-555-5555
|
|
18
|
+
Pattern.new(name: :phone,
|
|
19
|
+
regex: /\b\(?\d{3}\)?[-.\s]\d{3}[-.\s]\d{4}\b/,
|
|
20
|
+
description: "US phone number"),
|
|
21
|
+
# Credit card number: 13-19 digits, optional separators
|
|
22
|
+
Pattern.new(name: :credit_card,
|
|
23
|
+
regex: /\b(?:\d[ -]*?){13,19}\b/,
|
|
24
|
+
description: "Credit card number"),
|
|
25
|
+
# IPv4 address
|
|
26
|
+
Pattern.new(name: :ipv4,
|
|
27
|
+
regex: /\b(?:\d{1,3}\.){3}\d{1,3}\b/,
|
|
28
|
+
description: "IPv4 address"),
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
# All registered patterns, in registration order.
|
|
32
|
+
#
|
|
33
|
+
# @return [Array<Pattern>]
|
|
34
|
+
def self.all
|
|
35
|
+
DEFAULT_PATTERNS + @custom.to_a
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Select patterns by name.
|
|
39
|
+
#
|
|
40
|
+
# @param names [Array<Symbol>, :all] names to select, or :all
|
|
41
|
+
# @return [Array<Pattern>]
|
|
42
|
+
def self.select(names)
|
|
43
|
+
return all if names == :all
|
|
44
|
+
|
|
45
|
+
list = all
|
|
46
|
+
Array(names).filter_map { |n| list.find { |p| p.name == n } }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Register a custom pattern. Append-only.
|
|
50
|
+
#
|
|
51
|
+
# @param pattern [Pattern]
|
|
52
|
+
# @return [void]
|
|
53
|
+
def self.register(pattern)
|
|
54
|
+
(@custom ||= []) << pattern
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@custom = []
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Redact
|
|
5
|
+
# Aggregated redaction result.
|
|
6
|
+
class Result
|
|
7
|
+
attr_reader :by_pattern
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@by_pattern = Hash.new { |h, k| h[k] = 0 }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @param name [Symbol] pattern name
|
|
14
|
+
# @param count [Integer] substitutions for this pattern
|
|
15
|
+
# @return [void]
|
|
16
|
+
def add(name, count)
|
|
17
|
+
@by_pattern[name] += count
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Total substitutions across all patterns.
|
|
21
|
+
#
|
|
22
|
+
# @return [Integer]
|
|
23
|
+
def count
|
|
24
|
+
@by_pattern.values.sum
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Patterns that matched at least once.
|
|
28
|
+
#
|
|
29
|
+
# @return [Array<Symbol>]
|
|
30
|
+
def patterns_matched
|
|
31
|
+
@by_pattern.select { |_, c| c.positive? }.keys
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# True when zero substitutions were made.
|
|
35
|
+
#
|
|
36
|
+
# @return [Boolean]
|
|
37
|
+
def empty?
|
|
38
|
+
count.zero?
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
# Document redaction and anonymization.
|
|
5
|
+
#
|
|
6
|
+
# Built on FindReplace for content rewriting plus a metadata strip
|
|
7
|
+
# pass for identifying attributes (rsids, author info, revision
|
|
8
|
+
# metadata, hidden text). Compliance use case Word cannot serve.
|
|
9
|
+
module Redact
|
|
10
|
+
autoload :Engine, "#{__dir__}/redact/engine"
|
|
11
|
+
autoload :Pattern, "#{__dir__}/redact/pattern"
|
|
12
|
+
autoload :PatternLibrary, "#{__dir__}/redact/pattern_library"
|
|
13
|
+
autoload :Result, "#{__dir__}/redact/result"
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/uniword/version.rb
CHANGED
|
@@ -119,6 +119,82 @@ module Uniword
|
|
|
119
119
|
replacer.replace(self)
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
+
# Find and replace text across document parts.
|
|
123
|
+
#
|
|
124
|
+
# Mirrors Word's Home → Replace dialog (without the GUI).
|
|
125
|
+
# Replaces every non-overlapping match of `pattern` with
|
|
126
|
+
# `replacement` across the configured scopes. Returns a
|
|
127
|
+
# `FindReplace::Result` with per-scope counts.
|
|
128
|
+
#
|
|
129
|
+
# v1 limitation: matches inside a single run only. Matches
|
|
130
|
+
# that span run boundaries are silently skipped (Word does the
|
|
131
|
+
# same in default mode).
|
|
132
|
+
#
|
|
133
|
+
# @param pattern [String, Regexp] literal String or Regexp
|
|
134
|
+
# @param replacement [String] replacement text; for regex
|
|
135
|
+
# patterns, may reference captures via `\1`, `\2`, ...
|
|
136
|
+
# @param scope [Symbol, Array<Symbol>, :all] one or more of
|
|
137
|
+
# `:body`, `:headers`, `:footers`, `:footnotes`, `:endnotes`,
|
|
138
|
+
# `:comments`, `:styles`, or `:all` (default)
|
|
139
|
+
# @param ignore_case [Boolean] case-insensitive match
|
|
140
|
+
# @return [FindReplace::Result]
|
|
141
|
+
def find_replace(pattern, replacement, scope: :all, ignore_case: false)
|
|
142
|
+
matcher = build_find_replace_matcher(pattern, replacement, ignore_case)
|
|
143
|
+
FindReplace::Engine.new(document: self, matcher: matcher,
|
|
144
|
+
scopes: scope).run
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Turn change tracking on. Every subsequent edit is recorded as
|
|
148
|
+
# a tracked change. Mirrors Word's Review → Track Changes → On.
|
|
149
|
+
#
|
|
150
|
+
# @return [self]
|
|
151
|
+
def track_changes_on!
|
|
152
|
+
ensure_settings.track_changes = Wordprocessingml::TrackChanges.new
|
|
153
|
+
self
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Turn change tracking off. Existing tracked changes remain in
|
|
157
|
+
# the document; new edits are applied silently. Mirrors Word's
|
|
158
|
+
# Review → Track Changes → Off.
|
|
159
|
+
#
|
|
160
|
+
# @return [self]
|
|
161
|
+
def track_changes_off!
|
|
162
|
+
ensure_settings.track_changes = nil
|
|
163
|
+
self
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# True when change tracking is enabled.
|
|
167
|
+
#
|
|
168
|
+
# @return [Boolean]
|
|
169
|
+
def track_changes_enabled?
|
|
170
|
+
settings&.track_changes ? true : false
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Redact PII patterns and/or custom regex across the document.
|
|
174
|
+
#
|
|
175
|
+
# Built on FindReplace. Default pattern library matches US
|
|
176
|
+
# phone numbers, email, SSN, credit card numbers, IPv4
|
|
177
|
+
# addresses. Pass `patterns: :pii` for defaults, or an array
|
|
178
|
+
# of names (`[:ssn, :email]`) to select a subset.
|
|
179
|
+
#
|
|
180
|
+
# @param patterns [Symbol, Array<Symbol>, Array<Redact::Pattern>]
|
|
181
|
+
# `:pii` (default), or names/Pattern objects to apply
|
|
182
|
+
# @param scope [Symbol, Array<Symbol>, :all] find-replace scope
|
|
183
|
+
# @return [Redact::Result]
|
|
184
|
+
def redact(patterns: :pii, scope: :all)
|
|
185
|
+
Redact::Engine.new(document: self, patterns: patterns,
|
|
186
|
+
scope: scope).run
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Run a lint ruleset against the document.
|
|
190
|
+
#
|
|
191
|
+
# @param ruleset [Lint::Ruleset, Array<Lint::Rule>] rules to
|
|
192
|
+
# apply
|
|
193
|
+
# @return [Lint::Result]
|
|
194
|
+
def lint(ruleset:)
|
|
195
|
+
Lint::Engine.new(document: self, ruleset: ruleset).run
|
|
196
|
+
end
|
|
197
|
+
|
|
122
198
|
# Apply uniform page setup to every section of the document
|
|
123
199
|
#
|
|
124
200
|
# Mirrors Word's Layout dialog: named paper sizes, orientation
|
|
@@ -230,6 +306,21 @@ module Uniword
|
|
|
230
306
|
|
|
231
307
|
private
|
|
232
308
|
|
|
309
|
+
def build_find_replace_matcher(pattern, replacement, ignore_case)
|
|
310
|
+
if pattern.is_a?(Regexp)
|
|
311
|
+
FindReplace::RegexMatcher.new(pattern: pattern,
|
|
312
|
+
replacement: replacement)
|
|
313
|
+
else
|
|
314
|
+
FindReplace::StringMatcher.new(pattern: pattern,
|
|
315
|
+
replacement: replacement,
|
|
316
|
+
ignore_case: ignore_case)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def ensure_settings
|
|
321
|
+
self.settings ||= Wordprocessingml::Settings.new
|
|
322
|
+
end
|
|
323
|
+
|
|
233
324
|
# Ensure the document carries a theme part, creating it from a
|
|
234
325
|
# fresh parse of the bundled Office theme when absent (every
|
|
235
326
|
# document gets its own copy — no shared state).
|
|
@@ -37,6 +37,7 @@ module Uniword
|
|
|
37
37
|
attribute :hyphenation_zone, HyphenationZone
|
|
38
38
|
attribute :style_pane_sort_method, StylePaneSortMethod
|
|
39
39
|
attribute :doc_vars, DocVars
|
|
40
|
+
attribute :track_changes, TrackChanges
|
|
40
41
|
attribute :w14_doc_id, W14DocId
|
|
41
42
|
attribute :w15_chart_tracking_ref_based, W15ChartTrackingRefBased
|
|
42
43
|
attribute :w15_doc_id, W15DocId
|
|
@@ -106,6 +107,7 @@ module Uniword
|
|
|
106
107
|
map_element "endnotePr", to: :endnote_pr, render_nil: false
|
|
107
108
|
map_element "compat", to: :compat, render_nil: false
|
|
108
109
|
map_element "docVars", to: :doc_vars, render_nil: false
|
|
110
|
+
map_element "trackChanges", to: :track_changes, render_nil: false
|
|
109
111
|
map_element "rsids", to: :rsids, render_nil: false
|
|
110
112
|
map_element "mathPr", to: :math_pr, render_nil: false
|
|
111
113
|
map_element "themeFontLang", to: :theme_font_lang, render_nil: false
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Wordprocessingml
|
|
7
|
+
# Empty `<w:trackChanges/>` element in `<w:settings>` — Word's
|
|
8
|
+
# Review → Track Changes toggle.
|
|
9
|
+
#
|
|
10
|
+
# When present, Word records every edit as a tracked change. When
|
|
11
|
+
# absent, edits are applied silently.
|
|
12
|
+
class TrackChanges < Lutaml::Model::Serializable
|
|
13
|
+
xml do
|
|
14
|
+
element "trackChanges"
|
|
15
|
+
namespace Uniword::Ooxml::Namespaces::WordProcessingML
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -88,6 +88,7 @@ module Uniword
|
|
|
88
88
|
autoload :DefaultTabStop, "uniword/wordprocessingml/default_tab_stop"
|
|
89
89
|
autoload :CharacterSpacingControl, "uniword/wordprocessingml/character_spacing_control"
|
|
90
90
|
autoload :DoNotDisplayPageBoundaries, "uniword/wordprocessingml/do_not_display_page_boundaries"
|
|
91
|
+
autoload :TrackChanges, "uniword/wordprocessingml/track_changes"
|
|
91
92
|
autoload :Rsids, "uniword/wordprocessingml/rsids"
|
|
92
93
|
autoload :RsidRoot, "uniword/wordprocessingml/rsid_root"
|
|
93
94
|
autoload :Rsid, "uniword/wordprocessingml/rsid"
|
data/lib/uniword.rb
CHANGED
|
@@ -130,6 +130,16 @@ module Uniword
|
|
|
130
130
|
# Builder API (construction layer for OOXML models)
|
|
131
131
|
autoload :Builder, "uniword/builder"
|
|
132
132
|
|
|
133
|
+
# Find & replace over document parts (body, headers, footers,
|
|
134
|
+
# footnotes, endnotes, comments, styles).
|
|
135
|
+
autoload :FindReplace, "uniword/find_replace"
|
|
136
|
+
|
|
137
|
+
# Document redaction (PII patterns, custom regex).
|
|
138
|
+
autoload :Redact, "uniword/redact"
|
|
139
|
+
|
|
140
|
+
# Document lint (style-guide enforcement).
|
|
141
|
+
autoload :Lint, "uniword/lint"
|
|
142
|
+
|
|
133
143
|
# CLI
|
|
134
144
|
autoload :CLI, "uniword/cli/main"
|
|
135
145
|
autoload :GenerateCLI, "uniword/cli/generate_cli"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: uniword
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.5.
|
|
4
|
+
version: 1.5.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: logger
|
|
@@ -817,6 +817,7 @@ files:
|
|
|
817
817
|
- lib/uniword/docx.rb
|
|
818
818
|
- lib/uniword/docx/chart_part.rb
|
|
819
819
|
- lib/uniword/docx/custom_xml_item.rb
|
|
820
|
+
- lib/uniword/docx/deterministic_output.rb
|
|
820
821
|
- lib/uniword/docx/document_statistics.rb
|
|
821
822
|
- lib/uniword/docx/header_footer_part.rb
|
|
822
823
|
- lib/uniword/docx/header_footer_part_collection.rb
|
|
@@ -1001,6 +1002,21 @@ files:
|
|
|
1001
1002
|
- lib/uniword/endnote.rb
|
|
1002
1003
|
- lib/uniword/errors.rb
|
|
1003
1004
|
- lib/uniword/field.rb
|
|
1005
|
+
- lib/uniword/find_replace.rb
|
|
1006
|
+
- lib/uniword/find_replace/body_scope.rb
|
|
1007
|
+
- lib/uniword/find_replace/comment_scope.rb
|
|
1008
|
+
- lib/uniword/find_replace/endnote_scope.rb
|
|
1009
|
+
- lib/uniword/find_replace/engine.rb
|
|
1010
|
+
- lib/uniword/find_replace/footer_scope.rb
|
|
1011
|
+
- lib/uniword/find_replace/footnote_scope.rb
|
|
1012
|
+
- lib/uniword/find_replace/header_scope.rb
|
|
1013
|
+
- lib/uniword/find_replace/matcher.rb
|
|
1014
|
+
- lib/uniword/find_replace/paragraph_walker.rb
|
|
1015
|
+
- lib/uniword/find_replace/regex_matcher.rb
|
|
1016
|
+
- lib/uniword/find_replace/result.rb
|
|
1017
|
+
- lib/uniword/find_replace/scope.rb
|
|
1018
|
+
- lib/uniword/find_replace/string_matcher.rb
|
|
1019
|
+
- lib/uniword/find_replace/styles_scope.rb
|
|
1004
1020
|
- lib/uniword/footer.rb
|
|
1005
1021
|
- lib/uniword/footnote.rb
|
|
1006
1022
|
- lib/uniword/format_converter.rb
|
|
@@ -1048,6 +1064,16 @@ files:
|
|
|
1048
1064
|
- lib/uniword/infrastructure/zip_packager.rb
|
|
1049
1065
|
- lib/uniword/lazy_loader.rb
|
|
1050
1066
|
- lib/uniword/line_numbering.rb
|
|
1067
|
+
- lib/uniword/lint.rb
|
|
1068
|
+
- lib/uniword/lint/builtin_rules.rb
|
|
1069
|
+
- lib/uniword/lint/builtin_rules/banned_words.rb
|
|
1070
|
+
- lib/uniword/lint/builtin_rules/max_paragraph_length.rb
|
|
1071
|
+
- lib/uniword/lint/builtin_rules/require_body.rb
|
|
1072
|
+
- lib/uniword/lint/builtin_rules/required_style.rb
|
|
1073
|
+
- lib/uniword/lint/engine.rb
|
|
1074
|
+
- lib/uniword/lint/result.rb
|
|
1075
|
+
- lib/uniword/lint/rule.rb
|
|
1076
|
+
- lib/uniword/lint/ruleset.rb
|
|
1051
1077
|
- lib/uniword/loggable.rb
|
|
1052
1078
|
- lib/uniword/logger.rb
|
|
1053
1079
|
- lib/uniword/math.rb
|
|
@@ -1294,6 +1320,11 @@ files:
|
|
|
1294
1320
|
- lib/uniword/quality/rules/paragraph_length_rule.rb
|
|
1295
1321
|
- lib/uniword/quality/rules/style_consistency_rule.rb
|
|
1296
1322
|
- lib/uniword/quality/rules/table_header_rule.rb
|
|
1323
|
+
- lib/uniword/redact.rb
|
|
1324
|
+
- lib/uniword/redact/engine.rb
|
|
1325
|
+
- lib/uniword/redact/pattern.rb
|
|
1326
|
+
- lib/uniword/redact/pattern_library.rb
|
|
1327
|
+
- lib/uniword/redact/result.rb
|
|
1297
1328
|
- lib/uniword/resource.rb
|
|
1298
1329
|
- lib/uniword/resource/cache.rb
|
|
1299
1330
|
- lib/uniword/resource/cache_paths.rb
|
|
@@ -1849,6 +1880,7 @@ files:
|
|
|
1849
1880
|
- lib/uniword/wordprocessingml/theme_font_lang.rb
|
|
1850
1881
|
- lib/uniword/wordprocessingml/title_pg.rb
|
|
1851
1882
|
- lib/uniword/wordprocessingml/tr_height.rb
|
|
1883
|
+
- lib/uniword/wordprocessingml/track_changes.rb
|
|
1852
1884
|
- lib/uniword/wordprocessingml/ui_priority.rb
|
|
1853
1885
|
- lib/uniword/wordprocessingml/update_fields.rb
|
|
1854
1886
|
- lib/uniword/wordprocessingml/val_int.rb
|