Class: HTM::Models::Tag
- Inherits:
-
Object
- Object
- HTM::Models::Tag
- Defined in:
- lib/htm/models/tag.rb
Overview
Tag model - represents unique tag names Tags have a many-to-many relationship with nodes through node_tags
Constant Summary collapse
- TAG_FORMAT =
Tag name format regex
/\A[a-z0-9-]+(:[a-z0-9-]+)*\z/
Class Method Summary collapse
-
.calculate_node_positions(node, depth, positions, y_offset, h_spacing, v_spacing, parent_path = nil) ⇒ Object
Calculate node positions for SVG layout (internal helper).
-
.calculate_tree_stats(node, depth = 0) ⇒ Object
Calculate tree statistics (internal helper).
-
.empty_tree_svg(title) ⇒ Object
Generate SVG for empty tree (internal helper).
-
.exists?(conditions = {}) ⇒ Boolean
Check if a tag exists with the given conditions.
-
.expand_hierarchy(tag_name) ⇒ Array<String>
Expand a hierarchical tag name into all ancestor paths.
-
.find_by_topic_prefix(prefix) ⇒ Sequel::Dataset
Find tags with a given prefix (hierarchical query).
-
.find_or_create_by_name(name) ⇒ Tag
Find or create a tag by name.
-
.find_or_create_with_ancestors(name) ⇒ Array<Tag>
Find or create a tag and all its ancestor tags.
-
.format_tree_branch(node, is_last_array = []) ⇒ Object
Format a tree branch recursively (internal helper).
-
.generate_mermaid_nodes(node, parent_path, lines, node_ids, counter) ⇒ Object
Generate Mermaid nodes recursively (internal helper).
-
.generate_tree_svg(tree_data, positions, width, height, padding, node_width, node_height, title) ⇒ Object
Generate SVG tree visualization (internal helper).
-
.popular_tags(limit = 10) ⇒ Array<Tag>
Get the most frequently used tags.
- .svg_edge_path(x1, y1, x2, y2) ⇒ Object
- .svg_node_coords(pos, parent_pos, padding, node_width, node_height) ⇒ Object
- .svg_node_elements(path, pos, parent_pos, colors, padding, node_width, node_height) ⇒ Object
- .svg_node_label(x, y, node_width, node_height, label) ⇒ Object
- .svg_node_rect(x, y, node_width, node_height, color) ⇒ Object
- .svg_title_element(width, title) ⇒ Object
-
.tree ⇒ Hash
Returns a nested hash tree structure from the current scope.
-
.tree_mermaid(direction: 'TD') ⇒ String
Returns a Mermaid flowchart representation of the tag tree.
-
.tree_string ⇒ String
Returns a formatted string representation of the tag tree.
-
.tree_svg(title: 'HTM Tag Hierarchy') ⇒ String
Returns an SVG representation of the tag tree.
Instance Method Summary collapse
-
#before_create ⇒ Object
Hooks.
-
#deleted? ⇒ Boolean
Check if tag is soft-deleted.
-
#depth ⇒ Integer
Get the depth (number of levels) of this tag.
-
#hierarchical? ⇒ Boolean
Check if this tag is hierarchical (has child levels).
-
#restore! ⇒ Boolean
Restore a soft-deleted tag.
-
#root_topic ⇒ String
Get the root (top-level) topic of this tag.
-
#soft_delete! ⇒ Boolean
Soft delete - mark tag as deleted without removing from database.
-
#topic_levels ⇒ Array<String>
Get all hierarchy levels of this tag.
-
#usage_count ⇒ Integer
Get the number of nodes using this tag.
-
#validate ⇒ Object
Validations.
Class Method Details
.calculate_node_positions(node, depth, positions, y_offset, h_spacing, v_spacing, parent_path = nil) ⇒ Object
Calculate node positions for SVG layout (internal helper)
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 |
# File 'lib/htm/models/tag.rb', line 286 def self.calculate_node_positions(node, depth, positions, y_offset, h_spacing, v_spacing, parent_path = nil) node.keys.sort.each do |key| current_path = parent_path ? "#{parent_path}:#{key}" : key positions[current_path] = { x: depth, y: y_offset[0], label: key } y_offset[0] += 1 children = node[key] calculate_node_positions(children, depth + 1, positions, y_offset, h_spacing, v_spacing, current_path) unless children.empty? end end |
.calculate_tree_stats(node, depth = 0) ⇒ Object
Calculate tree statistics (internal helper)
270 271 272 273 274 275 276 277 278 279 280 281 282 283 |
# File 'lib/htm/models/tag.rb', line 270 def self.calculate_tree_stats(node, depth = 0) return { total_nodes: 0, max_depth: depth } if node.empty? total = node.keys.size max = depth + 1 node.each_value do |children| child_stats = calculate_tree_stats(children, depth + 1) total += child_stats[:total_nodes] max = [max, child_stats[:max_depth]].max end { total_nodes: total, max_depth: max } end |
.empty_tree_svg(title) ⇒ Object
Generate SVG for empty tree (internal helper)
303 304 305 306 307 308 309 310 311 |
# File 'lib/htm/models/tag.rb', line 303 def self.empty_tree_svg(title) <<~SVG <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 100"> <rect width="100%" height="100%" fill="transparent"/> <text x="150" y="30" text-anchor="middle" fill="#9CA3AF" font-family="system-ui, sans-serif" font-size="14" font-weight="bold">#{title}</text> <text x="150" y="60" text-anchor="middle" fill="#6B7280" font-family="system-ui, sans-serif" font-size="12">No tags found</text> </svg> SVG end |
.exists?(conditions = {}) ⇒ Boolean
Check if a tag exists with the given conditions
92 93 94 |
# File 'lib/htm/models/tag.rb', line 92 def self.exists?(conditions = {}) where(conditions).any? end |
.expand_hierarchy(tag_name) ⇒ Array<String>
Expand a hierarchical tag name into all ancestor paths
134 135 136 137 138 139 |
# File 'lib/htm/models/tag.rb', line 134 def self.(tag_name) return [] if tag_name.nil? || tag_name.empty? levels = tag_name.split(':') (1..levels.size).map { |i| levels[0, i].join(':') } end |
.find_by_topic_prefix(prefix) ⇒ Sequel::Dataset
Find tags with a given prefix (hierarchical query)
101 102 103 |
# File 'lib/htm/models/tag.rb', line 101 def self.find_by_topic_prefix(prefix) dataset.with_prefix(prefix) end |
.find_or_create_by_name(name) ⇒ Tag
Find or create a tag by name
125 126 127 |
# File 'lib/htm/models/tag.rb', line 125 def self.find_or_create_by_name(name) find_or_create(name: name) end |
.find_or_create_with_ancestors(name) ⇒ Array<Tag>
Find or create a tag and all its ancestor tags
146 147 148 149 150 |
# File 'lib/htm/models/tag.rb', line 146 def self.find_or_create_with_ancestors(name) (name).map do |tag_name| find_or_create(name: tag_name) end end |
.format_tree_branch(node, is_last_array = []) ⇒ Object
Format a tree branch recursively (internal helper)
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
# File 'lib/htm/models/tag.rb', line 227 def self.format_tree_branch(node, is_last_array = []) result = '' sorted_keys = node.keys.sort sorted_keys.each_with_index do |key, index| is_last = (index == sorted_keys.size - 1) line_prefix = is_last_array.map { |was_last| was_last ? ' ' : '| ' }.join result += "#{line_prefix}+-- #{key}\n" children = node[key] unless children.empty? result += format_tree_branch(children, is_last_array + [is_last]) end end result end |
.generate_mermaid_nodes(node, parent_path, lines, node_ids, counter) ⇒ Object
Generate Mermaid nodes recursively (internal helper)
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'lib/htm/models/tag.rb', line 248 def self.generate_mermaid_nodes(node, parent_path, lines, node_ids, counter) node.keys.sort.each do |key| current_path = parent_path ? "#{parent_path}:#{key}" : key node_id = "n#{counter}" node_ids[current_path] = node_id counter += 1 lines << " #{node_id}[\"#{key}\"]" if parent_path && node_ids[parent_path] lines << " #{node_ids[parent_path]} --> #{node_id}" end children = node[key] counter = generate_mermaid_nodes(children, current_path, lines, node_ids, counter) unless children.empty? end counter end |
.generate_tree_svg(tree_data, positions, width, height, padding, node_width, node_height, title) ⇒ Object
Generate SVG tree visualization (internal helper)
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 |
# File 'lib/htm/models/tag.rb', line 314 def self.generate_tree_svg(tree_data, positions, width, height, padding, node_width, node_height, title) colors = ['#3B82F6', '#8B5CF6', '#EC4899', '#F59E0B', '#10B981', '#6366F1'] svg_lines = [] svg_lines << %(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 #{width} #{height + 40}">) svg_lines << ' <rect width="100%" height="100%" fill="transparent"/>' svg_lines << svg_title_element(width, title) positions.each do |path, pos| parent_path = path.include?(':') ? path.split(':')[0..-2].join(':') : nil next unless parent_path && positions[parent_path] svg_lines.concat svg_node_elements(path, pos, positions[parent_path], colors, padding, node_width, node_height) end svg_lines << '</svg>' svg_lines.join("\n") end |
.popular_tags(limit = 10) ⇒ Array<Tag>
Get the most frequently used tags
110 111 112 113 114 115 116 117 118 |
# File 'lib/htm/models/tag.rb', line 110 def self.(limit = 10) dataset .select_append { count([:id]).as(usage_count) } .join(:node_tags, tag_id: :id) .group(:id) .order(Sequel.desc(:usage_count)) .limit(limit) .all end |
.svg_edge_path(x1, y1, x2, y2) ⇒ Object
356 357 358 359 |
# File 'lib/htm/models/tag.rb', line 356 def self.svg_edge_path(x1, y1, x2, y2) mid_x = (x1 + x2) / 2 %( <path d="M#{x1},#{y1} C#{mid_x},#{y1} #{mid_x},#{y2} #{x2},#{y2}" stroke="#4B5563" stroke-width="2" fill="none"/>) end |
.svg_node_coords(pos, parent_pos, padding, node_width, node_height) ⇒ Object
345 346 347 348 349 350 351 352 353 354 |
# File 'lib/htm/models/tag.rb', line 345 def self.svg_node_coords(pos, parent_pos, padding, node_width, node_height) x2 = padding + (pos[:x] * (node_width + 40)) y2 = 40 + padding + (pos[:y] * (node_height + 20)) + (node_height / 2) { x1: padding + (parent_pos[:x] * (node_width + 40)) + node_width, y1: 40 + padding + (parent_pos[:y] * (node_height + 20)) + (node_height / 2), x2: x2, y2: y2, x: x2, y: 40 + padding + (pos[:y] * (node_height + 20)) } end |
.svg_node_elements(path, pos, parent_pos, colors, padding, node_width, node_height) ⇒ Object
335 336 337 338 339 340 341 342 343 |
# File 'lib/htm/models/tag.rb', line 335 def self.svg_node_elements(path, pos, parent_pos, colors, padding, node_width, node_height) c = svg_node_coords(pos, parent_pos, padding, node_width, node_height) color = colors[path.count(':') % colors.size] [ svg_edge_path(c[:x1], c[:y1], c[:x2], c[:y2]), svg_node_rect(c[:x], c[:y], node_width, node_height, color), svg_node_label(c[:x], c[:y], node_width, node_height, pos[:label]) ] end |
.svg_node_label(x, y, node_width, node_height, label) ⇒ Object
365 366 367 |
# File 'lib/htm/models/tag.rb', line 365 def self.svg_node_label(x, y, node_width, node_height, label) %( <text x="#{x + (node_width / 2)}" y="#{y + (node_height / 2) + 4}" text-anchor="middle" fill="#FFFFFF" font-family="system-ui, sans-serif" font-size="11" font-weight="500">#{label}</text>) # rubocop:disable Layout/LineLength end |
.svg_node_rect(x, y, node_width, node_height, color) ⇒ Object
361 362 363 |
# File 'lib/htm/models/tag.rb', line 361 def self.svg_node_rect(x, y, node_width, node_height, color) %( <rect x="#{x}" y="#{y}" width="#{node_width}" height="#{node_height}" rx="6" fill="#{color}" opacity="0.9"/>) end |
.svg_title_element(width, title) ⇒ Object
331 332 333 |
# File 'lib/htm/models/tag.rb', line 331 def self.svg_title_element(width, title) %( <text x="#{width / 2}" y="25" text-anchor="middle" fill="#F3F4F6" font-family="system-ui, sans-serif" font-size="16" font-weight="bold">#{title}</text>) # rubocop:disable Layout/LineLength end |
.tree ⇒ Hash
Returns a nested hash tree structure from the current scope
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/htm/models/tag.rb', line 156 def self.tree tree = {} order(:name).select_map(:name).each do |tag_name| parts = tag_name.split(':') current = tree parts.each do |part| current[part] ||= {} current = current[part] end end tree end |
.tree_mermaid(direction: 'TD') ⇒ String
Returns a Mermaid flowchart representation of the tag tree
185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/htm/models/tag.rb', line 185 def self.tree_mermaid(direction: 'TD') tree_data = tree return "flowchart #{direction}\n empty[No tags]" if tree_data.empty? lines = ["flowchart #{direction}"] node_id = 0 node_ids = {} generate_mermaid_nodes(tree_data, nil, lines, node_ids, node_id) lines.join("\n") end |
.tree_string ⇒ String
Returns a formatted string representation of the tag tree
176 177 178 |
# File 'lib/htm/models/tag.rb', line 176 def self.tree_string format_tree_branch(tree) end |
.tree_svg(title: 'HTM Tag Hierarchy') ⇒ String
Returns an SVG representation of the tag tree
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 |
# File 'lib/htm/models/tag.rb', line 203 def self.tree_svg(title: 'HTM Tag Hierarchy') tree_data = tree return empty_tree_svg(title) if tree_data.empty? stats = calculate_tree_stats(tree_data) max_depth = stats[:max_depth] node_width = 140 node_height = 30 h_spacing = 180 v_spacing = 50 padding = 40 positions = {} y_offset = [0] calculate_node_positions(tree_data, 0, positions, y_offset, h_spacing, v_spacing) width = (max_depth * h_spacing) + node_width + (padding * 2) height = (y_offset[0] * v_spacing) + node_height + (padding * 2) generate_tree_svg(tree_data, positions, width, height, padding, node_width, node_height, title) end |
Instance Method Details
#before_create ⇒ Object
Hooks
80 81 82 83 |
# File 'lib/htm/models/tag.rb', line 80 def before_create self.created_at ||= Time.now super end |
#deleted? ⇒ Boolean
Check if tag is soft-deleted
433 434 435 |
# File 'lib/htm/models/tag.rb', line 433 def deleted? !deleted_at.nil? end |
#depth ⇒ Integer
Get the depth (number of levels) of this tag
391 392 393 |
# File 'lib/htm/models/tag.rb', line 391 def depth topic_levels.length end |
#hierarchical? ⇒ Boolean
Check if this tag is hierarchical (has child levels)
399 400 401 |
# File 'lib/htm/models/tag.rb', line 399 def hierarchical? name.include?(':') end |
#restore! ⇒ Boolean
Restore a soft-deleted tag
424 425 426 427 |
# File 'lib/htm/models/tag.rb', line 424 def restore! update(deleted_at: nil) true end |
#root_topic ⇒ String
Get the root (top-level) topic of this tag
375 376 377 |
# File 'lib/htm/models/tag.rb', line 375 def root_topic name.split(':').first end |
#soft_delete! ⇒ Boolean
Soft delete - mark tag as deleted without removing from database
415 416 417 418 |
# File 'lib/htm/models/tag.rb', line 415 def soft_delete! update(deleted_at: Time.now) true end |
#topic_levels ⇒ Array<String>
Get all hierarchy levels of this tag
383 384 385 |
# File 'lib/htm/models/tag.rb', line 383 def topic_levels name.split(':') end |
#usage_count ⇒ Integer
Get the number of nodes using this tag
407 408 409 |
# File 'lib/htm/models/tag.rb', line 407 def usage_count .count end |
#validate ⇒ Object
Validations
21 22 23 24 25 26 27 |
# File 'lib/htm/models/tag.rb', line 21 def validate super validates_presence :name validates_format TAG_FORMAT, :name, message: "must be lowercase with hyphens, using colons for hierarchy (e.g., 'database:postgresql:performance')" validates_unique :name, message: "already exists" end |