archsight 0.1.1 → 0.1.3
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/Dockerfile +21 -22
- data/README.md +38 -12
- data/exe/archsight +3 -1
- data/lib/archsight/analysis/executor.rb +112 -0
- data/lib/archsight/analysis/result.rb +174 -0
- data/lib/archsight/analysis/sandbox.rb +319 -0
- data/lib/archsight/analysis.rb +11 -0
- data/lib/archsight/annotations/architecture_annotations.rb +2 -2
- data/lib/archsight/cli.rb +164 -0
- data/lib/archsight/database.rb +6 -2
- data/lib/archsight/helpers/analysis_renderer.rb +83 -0
- data/lib/archsight/helpers/formatting.rb +95 -0
- data/lib/archsight/helpers.rb +20 -4
- data/lib/archsight/import/concurrent_progress.rb +341 -0
- data/lib/archsight/import/executor.rb +466 -0
- data/lib/archsight/import/git_analytics.rb +626 -0
- data/lib/archsight/import/handler.rb +263 -0
- data/lib/archsight/import/handlers/github.rb +161 -0
- data/lib/archsight/import/handlers/gitlab.rb +202 -0
- data/lib/archsight/import/handlers/jira_base.rb +189 -0
- data/lib/archsight/import/handlers/jira_discover.rb +161 -0
- data/lib/archsight/import/handlers/jira_metrics.rb +179 -0
- data/lib/archsight/import/handlers/openapi_schema_parser.rb +279 -0
- data/lib/archsight/import/handlers/repository.rb +439 -0
- data/lib/archsight/import/handlers/rest_api.rb +293 -0
- data/lib/archsight/import/handlers/rest_api_index.rb +183 -0
- data/lib/archsight/import/progress.rb +91 -0
- data/lib/archsight/import/registry.rb +54 -0
- data/lib/archsight/import/shared_file_writer.rb +67 -0
- data/lib/archsight/import/team_matcher.rb +195 -0
- data/lib/archsight/import.rb +14 -0
- data/lib/archsight/resources/analysis.rb +91 -0
- data/lib/archsight/resources/application_component.rb +2 -2
- data/lib/archsight/resources/application_service.rb +12 -12
- data/lib/archsight/resources/business_product.rb +12 -12
- data/lib/archsight/resources/data_object.rb +1 -1
- data/lib/archsight/resources/import.rb +79 -0
- data/lib/archsight/resources/technology_artifact.rb +23 -2
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/docs.rb +17 -0
- data/lib/archsight/web/api/json_helpers.rb +164 -0
- data/lib/archsight/web/api/openapi/spec.yaml +500 -0
- data/lib/archsight/web/api/routes.rb +101 -0
- data/lib/archsight/web/application.rb +66 -43
- data/lib/archsight/web/doc/import.md +458 -0
- data/lib/archsight/web/doc/index.md.erb +2 -1
- data/lib/archsight/web/public/css/artifact.css +10 -0
- data/lib/archsight/web/public/css/graph.css +14 -0
- data/lib/archsight/web/public/css/instance.css +489 -0
- data/lib/archsight/web/views/api_docs.erb +19 -0
- data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +14 -8
- data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +74 -0
- data/lib/archsight/web/views/partials/instance/_analysis_result.haml +64 -0
- data/lib/archsight/web/views/partials/instance/_detail.haml +7 -3
- data/lib/archsight/web/views/partials/instance/_import_detail.haml +87 -0
- data/lib/archsight/web/views/partials/instance/_relations.haml +4 -4
- data/lib/archsight/web/views/partials/layout/_content.haml +4 -0
- data/lib/archsight/web/views/partials/layout/_navigation.haml +6 -5
- metadata +78 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 697c02b1e33af4fde32636af6020b7ab6513245216b694453d03dab186f0dd99
|
|
4
|
+
data.tar.gz: 58a8cd9b7e530796d04424f885e931863a1bf6ed2c2d0366bf76d28453eb80fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a80fc22f8b7030c71cb2422b839521e96f31ecced71386ed075c51d9ca2d53d2a057ced240a4085ad032c895ce8526d44a585b6970c67f2217fe15232a2209cd
|
|
7
|
+
data.tar.gz: 3d0648dca25e6337679d6a236b256b5580d9a5260df927a792680e5e9187e289b0c781aa36d0fb81893fea3889e57b7ec1b7a397d731039d5f5dcbebdff2f587
|
data/Dockerfile
CHANGED
|
@@ -1,39 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
# Build stage
|
|
2
|
+
FROM ruby:4.0-alpine3.23 AS builder
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
RUN apk add --no-cache \
|
|
5
|
-
build-base \
|
|
6
|
-
git \
|
|
7
|
-
libffi-dev \
|
|
8
|
-
yaml-dev \
|
|
9
|
-
graphviz
|
|
4
|
+
RUN apk add --no-cache build-base git libffi-dev yaml-dev
|
|
10
5
|
|
|
11
|
-
# Set working directory
|
|
12
6
|
WORKDIR /app
|
|
7
|
+
COPY . .
|
|
8
|
+
|
|
9
|
+
# Build and install the gem
|
|
10
|
+
RUN gem build archsight.gemspec && \
|
|
11
|
+
gem install --no-document archsight-*.gem
|
|
13
12
|
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
COPY lib/archsight/version.rb lib/archsight/version.rb
|
|
13
|
+
# Runtime stage
|
|
14
|
+
FROM ruby:4.0-alpine3.23
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
RUN bundle install --jobs 4
|
|
16
|
+
RUN apk add --no-cache graphviz
|
|
20
17
|
|
|
21
|
-
# Copy
|
|
22
|
-
COPY
|
|
18
|
+
# Copy installed gems from builder (including default gems that were updated)
|
|
19
|
+
COPY --from=builder /usr/local/bundle /usr/local/bundle
|
|
20
|
+
COPY --from=builder /usr/local/lib/ruby/gems /usr/local/lib/ruby/gems
|
|
21
|
+
|
|
22
|
+
# Ensure Ruby finds gems in both /usr/local/bundle and default gems location
|
|
23
|
+
ENV GEM_HOME=/usr/local/bundle
|
|
24
|
+
ENV GEM_PATH=/usr/local/bundle:/usr/local/lib/ruby/gems/4.0.0
|
|
25
|
+
ENV PATH="/usr/local/bundle/bin:${PATH}"
|
|
23
26
|
|
|
24
|
-
# Create volume mount point for resources
|
|
25
27
|
RUN mkdir -p /resources
|
|
26
28
|
|
|
27
|
-
# Set resources directory environment variable
|
|
28
29
|
ENV ARCHSIGHT_RESOURCES_DIR=/resources
|
|
29
30
|
ENV APP_ENV=production
|
|
30
31
|
|
|
31
|
-
# Expose port for web server
|
|
32
32
|
EXPOSE 4567
|
|
33
33
|
|
|
34
|
-
# Health check
|
|
35
34
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
36
35
|
CMD wget --no-verbose --tries=1 --spider http://localhost:4567/ || exit 1
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
CMD ["
|
|
37
|
+
ENTRYPOINT ["archsight"]
|
|
38
|
+
CMD ["web", "--port", "4567"]
|
data/README.md
CHANGED
|
@@ -41,29 +41,55 @@ Access at: <http://localhost:4567>
|
|
|
41
41
|
### Option 2: Docker
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
|
|
45
|
-
docker run -p 4567:4567 -v "/path/to/resources:/resources" archsight
|
|
44
|
+
# Run web server (default)
|
|
45
|
+
docker run -p 4567:4567 -v "/path/to/resources:/resources" ghcr.io/ionos-cloud/archsight
|
|
46
|
+
|
|
47
|
+
# Run in production mode with logging
|
|
48
|
+
docker run -p 4567:4567 -v "/path/to/resources:/resources" ghcr.io/ionos-cloud/archsight web --production
|
|
49
|
+
|
|
50
|
+
# Run lint
|
|
51
|
+
docker run -v "/path/to/resources:/resources" ghcr.io/ionos-cloud/archsight lint -r /resources
|
|
52
|
+
|
|
53
|
+
# Run any command
|
|
54
|
+
docker run ghcr.io/ionos-cloud/archsight version
|
|
46
55
|
```
|
|
47
56
|
|
|
48
|
-
Access at: <http://localhost:4567>
|
|
57
|
+
Access web interface at: <http://localhost:4567>
|
|
49
58
|
|
|
50
59
|
**Notes:**
|
|
51
60
|
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
- Health check verifies the service is running
|
|
61
|
+
- Volume mount `-v "/path/to/resources:/resources"` mounts your resources directory
|
|
62
|
+
- Default command starts the web server on port 4567
|
|
63
|
+
- Pass subcommands directly (lint, version, console, template)
|
|
56
64
|
|
|
57
65
|
## CLI Commands
|
|
58
66
|
|
|
59
67
|
```bash
|
|
60
|
-
archsight web [
|
|
61
|
-
archsight lint
|
|
62
|
-
archsight
|
|
63
|
-
archsight
|
|
64
|
-
archsight
|
|
68
|
+
archsight web [OPTIONS] # Start web server
|
|
69
|
+
archsight lint # Validate YAML and relations
|
|
70
|
+
archsight import # Execute pending imports
|
|
71
|
+
archsight analyze # Execute analysis scripts
|
|
72
|
+
archsight template KIND # Generate YAML template for a resource type
|
|
73
|
+
archsight console # Interactive Ruby console
|
|
74
|
+
archsight version # Show version
|
|
65
75
|
```
|
|
66
76
|
|
|
77
|
+
### Web Server Options
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
archsight web [--resources PATH] [--port PORT] [--host HOST]
|
|
81
|
+
[--production] [--disable-reload] [--enable-logging]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Option | Description |
|
|
85
|
+
|--------|-------------|
|
|
86
|
+
| `-r, --resources PATH` | Path to resources directory |
|
|
87
|
+
| `-p, --port PORT` | Port to listen on (default: 4567) |
|
|
88
|
+
| `-H, --host HOST` | Host to bind to (default: localhost) |
|
|
89
|
+
| `--production` | Run in production mode (quiet startup) |
|
|
90
|
+
| `--disable-reload` | Disable the reload button in the UI |
|
|
91
|
+
| `--enable-logging` | Enable request logging (default: false in dev, true in prod) |
|
|
92
|
+
|
|
67
93
|
## Features
|
|
68
94
|
|
|
69
95
|
### MCP Server
|
data/exe/archsight
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
# Support running from source (development)
|
|
5
|
+
lib_path = File.expand_path("../lib", __dir__)
|
|
6
|
+
$LOAD_PATH.unshift(lib_path) if File.directory?(File.join(lib_path, "archsight"))
|
|
5
7
|
|
|
6
8
|
require "archsight"
|
|
7
9
|
require "archsight/cli"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
require_relative "sandbox"
|
|
5
|
+
require_relative "result"
|
|
6
|
+
|
|
7
|
+
module Archsight
|
|
8
|
+
module Analysis
|
|
9
|
+
# Executor runs Analysis scripts in a sandboxed environment with timeout enforcement
|
|
10
|
+
class Executor
|
|
11
|
+
# Default timeout in seconds
|
|
12
|
+
DEFAULT_TIMEOUT = 30
|
|
13
|
+
|
|
14
|
+
# Duration parsing patterns
|
|
15
|
+
DURATION_PATTERNS = {
|
|
16
|
+
/^(\d+)s$/ => 1,
|
|
17
|
+
/^(\d+)m$/ => 60,
|
|
18
|
+
/^(\d+)h$/ => 3600
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :database
|
|
22
|
+
|
|
23
|
+
# @param database [Archsight::Database] Loaded database instance
|
|
24
|
+
def initialize(database)
|
|
25
|
+
@database = database
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Execute an Analysis resource
|
|
29
|
+
# @param analysis [Archsight::Resources::Analysis] Analysis to execute
|
|
30
|
+
# @return [Archsight::Analysis::Result] Execution result
|
|
31
|
+
def execute(analysis)
|
|
32
|
+
script = analysis.annotations["analysis/script"]
|
|
33
|
+
timeout_seconds = parse_timeout(analysis.annotations["analysis/timeout"])
|
|
34
|
+
|
|
35
|
+
return Result.new(analysis, success: false, error: "No script defined") if script.nil? || script.empty?
|
|
36
|
+
|
|
37
|
+
sandbox = Sandbox.new(@database)
|
|
38
|
+
sandbox._set_analysis(analysis)
|
|
39
|
+
|
|
40
|
+
start_time = Time.now
|
|
41
|
+
begin
|
|
42
|
+
Timeout.timeout(timeout_seconds) do
|
|
43
|
+
# Execute script in sandbox context
|
|
44
|
+
# Using instance_eval ensures script only has access to sandbox methods
|
|
45
|
+
sandbox.instance_eval(script, "analysis:#{analysis.name}", 1)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
duration = Time.now - start_time
|
|
49
|
+
Result.new(
|
|
50
|
+
analysis,
|
|
51
|
+
success: true,
|
|
52
|
+
sections: sandbox.sections,
|
|
53
|
+
duration: duration
|
|
54
|
+
)
|
|
55
|
+
rescue Timeout::Error
|
|
56
|
+
Result.new(
|
|
57
|
+
analysis,
|
|
58
|
+
success: false,
|
|
59
|
+
error: "Execution timed out after #{timeout_seconds}s",
|
|
60
|
+
sections: sandbox.sections
|
|
61
|
+
)
|
|
62
|
+
rescue StandardError, SyntaxError => e
|
|
63
|
+
Result.new(
|
|
64
|
+
analysis,
|
|
65
|
+
success: false,
|
|
66
|
+
error: "#{e.class}: #{e.message}",
|
|
67
|
+
error_backtrace: e.backtrace&.first(5),
|
|
68
|
+
sections: sandbox.sections
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Execute all enabled Analysis resources
|
|
74
|
+
# @param filter [Regexp, nil] Optional filter for analysis names
|
|
75
|
+
# @return [Array<Archsight::Analysis::Result>] Array of results
|
|
76
|
+
def execute_all(filter: nil)
|
|
77
|
+
analyses = @database.instances_by_kind("Analysis").values
|
|
78
|
+
|
|
79
|
+
# Filter by name pattern if provided
|
|
80
|
+
analyses = analyses.select { |a| filter.match?(a.name) } if filter
|
|
81
|
+
|
|
82
|
+
# Filter to enabled analyses
|
|
83
|
+
analyses = analyses.select { |a| analysis_enabled?(a) }
|
|
84
|
+
|
|
85
|
+
analyses.map { |analysis| execute(analysis) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# Parse timeout string to seconds
|
|
91
|
+
# @param timeout_str [String, nil] Timeout string (e.g., "30s", "5m")
|
|
92
|
+
# @return [Integer] Timeout in seconds
|
|
93
|
+
def parse_timeout(timeout_str)
|
|
94
|
+
return DEFAULT_TIMEOUT if timeout_str.nil? || timeout_str.empty?
|
|
95
|
+
|
|
96
|
+
DURATION_PATTERNS.each do |pattern, multiplier|
|
|
97
|
+
match = timeout_str.match(pattern)
|
|
98
|
+
return match[1].to_i * multiplier if match
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
DEFAULT_TIMEOUT
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Check if analysis is enabled
|
|
105
|
+
# @param analysis [Object] Analysis instance
|
|
106
|
+
# @return [Boolean] true if enabled
|
|
107
|
+
def analysis_enabled?(analysis)
|
|
108
|
+
analysis.annotations["analysis/enabled"] != "false"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archsight
|
|
4
|
+
module Analysis
|
|
5
|
+
# Result represents the outcome of an Analysis execution
|
|
6
|
+
class Result
|
|
7
|
+
attr_reader :analysis, :success, :error, :error_backtrace, :sections, :duration
|
|
8
|
+
|
|
9
|
+
# @param analysis [Object] The Analysis resource that was executed
|
|
10
|
+
# @param success [Boolean] Whether execution completed successfully
|
|
11
|
+
# @param error [String, nil] Error message if execution failed
|
|
12
|
+
# @param error_backtrace [Array<String>, nil] Backtrace if execution failed
|
|
13
|
+
# @param sections [Array<Hash>] Output sections generated during execution
|
|
14
|
+
# @param duration [Float, nil] Execution duration in seconds
|
|
15
|
+
def initialize(analysis, success:, error: nil, error_backtrace: nil, sections: [], duration: nil)
|
|
16
|
+
@analysis = analysis
|
|
17
|
+
@success = success
|
|
18
|
+
@error = error
|
|
19
|
+
@error_backtrace = error_backtrace
|
|
20
|
+
@sections = sections || []
|
|
21
|
+
@duration = duration
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Boolean] true if execution succeeded
|
|
25
|
+
def success?
|
|
26
|
+
@success
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Boolean] true if execution failed
|
|
30
|
+
def failed?
|
|
31
|
+
!@success
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [String] Analysis name
|
|
35
|
+
def name
|
|
36
|
+
@analysis.name
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [Boolean] true if any content sections exist (excluding messages)
|
|
40
|
+
def has_findings?
|
|
41
|
+
@sections.any? { |s| %i[table list text heading code].include?(s[:type]) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Integer] Count of error-level messages
|
|
45
|
+
def error_count
|
|
46
|
+
@sections.count { |s| s[:type] == :message && s[:level] == :error }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Integer] Count of warning-level messages
|
|
50
|
+
def warning_count
|
|
51
|
+
@sections.count { |s| s[:type] == :message && s[:level] == :warning }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Convert result to markdown (only script-generated content)
|
|
55
|
+
# @param verbose [Boolean] Include detailed output
|
|
56
|
+
# @return [String] Markdown formatted output
|
|
57
|
+
def to_markdown(verbose: false)
|
|
58
|
+
format_sections_markdown(verbose).compact.join("\n\n")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Render markdown for CLI display using tty-markdown
|
|
62
|
+
# @param verbose [Boolean] Include detailed output
|
|
63
|
+
# @return [String] Rendered output for terminal
|
|
64
|
+
def render(verbose: false)
|
|
65
|
+
require "tty-markdown"
|
|
66
|
+
md = to_markdown(verbose: verbose)
|
|
67
|
+
md.empty? ? "" : TTY::Markdown.parse(md)
|
|
68
|
+
rescue LoadError
|
|
69
|
+
# Fallback to plain markdown if tty-markdown not available
|
|
70
|
+
to_markdown(verbose: verbose)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Format result for console output (backward compatible)
|
|
74
|
+
# @param verbose [Boolean] Include detailed output
|
|
75
|
+
# @return [String] Formatted output
|
|
76
|
+
def to_s(verbose: false)
|
|
77
|
+
render(verbose: verbose)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Status emoji for display
|
|
81
|
+
# @return [String] Status emoji
|
|
82
|
+
def status_emoji
|
|
83
|
+
return "❌" unless success?
|
|
84
|
+
|
|
85
|
+
has_findings? ? "⚠️" : "✅"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Formatted duration string
|
|
89
|
+
# @return [String] Duration string or empty
|
|
90
|
+
def duration_str
|
|
91
|
+
@duration ? format("%.2fs", @duration) : ""
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Error details as markdown (for CLI to use if needed)
|
|
95
|
+
# @param verbose [Boolean] Include backtrace
|
|
96
|
+
# @return [String, nil] Error markdown or nil
|
|
97
|
+
def error_markdown(verbose: false)
|
|
98
|
+
return nil unless failed?
|
|
99
|
+
|
|
100
|
+
lines = ["**Error:** #{@error}"]
|
|
101
|
+
if verbose && @error_backtrace&.any?
|
|
102
|
+
lines << ""
|
|
103
|
+
lines << "```"
|
|
104
|
+
lines.concat(@error_backtrace)
|
|
105
|
+
lines << "```"
|
|
106
|
+
end
|
|
107
|
+
lines.join("\n")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def format_sections_markdown(verbose)
|
|
113
|
+
@sections.map { |section| format_section_markdown(section, verbose) }.compact
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def format_section_markdown(section, verbose)
|
|
117
|
+
case section[:type]
|
|
118
|
+
when :message then format_message_markdown(section)
|
|
119
|
+
when :heading then format_heading_markdown(section)
|
|
120
|
+
when :text then section[:content]
|
|
121
|
+
when :table then format_table_markdown(section, verbose)
|
|
122
|
+
when :list then format_list_markdown(section, verbose)
|
|
123
|
+
when :code then format_code_markdown(section)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def format_message_markdown(section)
|
|
128
|
+
emoji = { error: "🔴", warning: "🟡", info: "🔵" }.fetch(section[:level], "ℹ️")
|
|
129
|
+
"#{emoji} #{section[:message]}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def format_heading_markdown(section)
|
|
133
|
+
"#{"#" * (section[:level] + 1)} #{section[:text]}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def format_table_markdown(section, verbose)
|
|
137
|
+
headers = section[:headers]
|
|
138
|
+
rows = section[:rows]
|
|
139
|
+
|
|
140
|
+
# Limit rows if not verbose
|
|
141
|
+
display_rows = verbose ? rows : rows.first(10)
|
|
142
|
+
truncated = !verbose && rows.size > 10
|
|
143
|
+
|
|
144
|
+
lines = []
|
|
145
|
+
lines << "| #{headers.join(" | ")} |"
|
|
146
|
+
lines << "| #{headers.map { "---" }.join(" | ")} |"
|
|
147
|
+
display_rows.each do |row|
|
|
148
|
+
lines << "| #{row.map { |cell| cell.to_s.gsub("|", "\\|") }.join(" | ")} |"
|
|
149
|
+
end
|
|
150
|
+
lines << "_...and #{rows.size - 10} more rows_" if truncated
|
|
151
|
+
|
|
152
|
+
lines.join("\n")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def format_list_markdown(section, verbose)
|
|
156
|
+
items = section[:items]
|
|
157
|
+
|
|
158
|
+
# Limit items if not verbose
|
|
159
|
+
display_items = verbose ? items : items.first(10)
|
|
160
|
+
truncated = !verbose && items.size > 10
|
|
161
|
+
|
|
162
|
+
lines = display_items.map { |item| "- #{item}" }
|
|
163
|
+
lines << "_...and #{items.size - 10} more items_" if truncated
|
|
164
|
+
|
|
165
|
+
lines.join("\n")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def format_code_markdown(section)
|
|
169
|
+
lang = section[:lang] || ""
|
|
170
|
+
"```#{lang}\n#{section[:content]}\n```"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|