summaryrefslogtreecommitdiff
path: root/scripts/lib/glfm/update_specification.rb
blob: 73c23d40de5b16ccda723f20b7da056d35bd8d0e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# frozen_string_literal: true
require 'fileutils'
require 'open-uri'
require 'pathname'
require_relative 'constants'
require_relative 'shared'

module Glfm
  class UpdateSpecification
    include Constants
    include Shared

    def process
      output('Updating specification...')
      ghfm_spec_txt_lines = load_ghfm_spec_txt
      glfm_spec_txt_string = build_glfm_spec_txt(ghfm_spec_txt_lines)
      write_glfm_spec_txt(glfm_spec_txt_string)
    end

    private

    def load_ghfm_spec_txt
      # We only re-download the GitHub Flavored Markdown specification if the
      # UPDATE_GHFM_SPEC_TXT environment variable is set to true, which should only
      # ever be done manually and locally, never in CI. This provides some security
      # protection against a possible injection attack vector, if the GitHub-hosted
      # version of the spec is ever temporarily compromised with an injection attack.
      #
      # This also avoids doing external network access to download the file
      # in CI jobs, which can avoid potentially flaky builds if the GitHub-hosted
      # version of the file is temporarily unavailable.
      if ENV['UPDATE_GHFM_SPEC_TXT'] == 'true'
        download_and_write_ghfm_spec_txt
      else
        read_existing_ghfm_spec_txt
      end
    end

    def read_existing_ghfm_spec_txt
      output("Reading existing #{GHFM_SPEC_TXT_PATH}...")
      File.open(GHFM_SPEC_TXT_PATH).readlines
    end

    def download_and_write_ghfm_spec_txt
      output("Downloading #{GHFM_SPEC_TXT_URI}...")
      ghfm_spec_txt_uri_io = URI.open(GHFM_SPEC_TXT_URI)

      # Read IO stream into an array of lines for easy processing later
      ghfm_spec_txt_lines = ghfm_spec_txt_uri_io.readlines
      raise "Unable to read lines from #{GHFM_SPEC_TXT_URI}" if ghfm_spec_txt_lines.empty?

      # Make sure the GHFM spec version has not changed
      validate_expected_spec_version!(ghfm_spec_txt_lines[2])

      # Reset IO stream and re-read into a single string for easy writing
      # noinspection RubyNilAnalysis
      ghfm_spec_txt_uri_io.seek(0)
      ghfm_spec_txt_string = ghfm_spec_txt_uri_io.read
      raise "Unable to read string from #{GHFM_SPEC_TXT_URI}" unless ghfm_spec_txt_string

      output("Writing #{GHFM_SPEC_TXT_PATH}...")
      GHFM_SPEC_TXT_PATH.dirname.mkpath
      write_file(GHFM_SPEC_TXT_PATH, ghfm_spec_txt_string)

      ghfm_spec_txt_lines
    end

    def validate_expected_spec_version!(version_line)
      return if version_line =~ /\Aversion: #{GHFM_SPEC_VERSION}\Z/o

      raise "GitHub Flavored Markdown spec.txt version mismatch! " \
          "Expected 'version: #{GHFM_SPEC_VERSION}', got '#{version_line}'"
    end

    def build_glfm_spec_txt(ghfm_spec_txt_lines)
      glfm_spec_txt_lines = ghfm_spec_txt_lines.dup
      replace_header(glfm_spec_txt_lines)
      replace_intro_section(glfm_spec_txt_lines)
      insert_examples_txt(glfm_spec_txt_lines)
      glfm_spec_txt_lines.join('')
    end

    def replace_header(spec_txt_lines)
      spec_txt_lines[0, spec_txt_lines.index("...\n") + 1] = GLFM_SPEC_TXT_HEADER
    end

    def replace_intro_section(spec_txt_lines)
      glfm_intro_txt_lines = File.open(GLFM_INTRO_TXT_PATH).readlines
      raise "Unable to read lines from #{GLFM_INTRO_TXT_PATH}" if glfm_intro_txt_lines.empty?

      ghfm_intro_header_begin_index = spec_txt_lines.index do |line|
        line =~ INTRODUCTION_HEADER_LINE_TEXT
      end
      raise "Unable to locate introduction header line in #{GHFM_SPEC_TXT_PATH}" if ghfm_intro_header_begin_index.nil?

      # Find the index of the next header after the introduction header, starting from the index
      # of the introduction header this is the length of the intro section
      ghfm_intro_section_length = spec_txt_lines[ghfm_intro_header_begin_index + 1..].index do |line|
        line.start_with?('# ')
      end

      # Replace the intro section with the GitLab flavored Markdown intro section
      spec_txt_lines[ghfm_intro_header_begin_index, ghfm_intro_section_length] = glfm_intro_txt_lines
    end

    def insert_examples_txt(spec_txt_lines)
      glfm_examples_txt_lines = File.open(GLFM_EXAMPLES_TXT_PATH).readlines
      raise "Unable to read lines from #{GLFM_EXAMPLES_TXT_PATH}" if glfm_examples_txt_lines.empty?

      ghfm_end_tests_comment_index = spec_txt_lines.index do |line|
        line =~ END_TESTS_COMMENT_LINE_TEXT
      end
      raise "Unable to locate 'END TESTS' comment line in #{GHFM_SPEC_TXT_PATH}" if ghfm_end_tests_comment_index.nil?

      # Insert the GLFM examples before the 'END TESTS' comment line
      spec_txt_lines[ghfm_end_tests_comment_index - 1] = ["\n", glfm_examples_txt_lines, "\n"].flatten

      spec_txt_lines
    end

    def write_glfm_spec_txt(glfm_spec_txt_string)
      output("Writing #{GLFM_SPEC_TXT_PATH}...")
      FileUtils.mkdir_p(Pathname.new(GLFM_SPEC_TXT_PATH).dirname)
      write_file(GLFM_SPEC_TXT_PATH, glfm_spec_txt_string)
    end
  end
end