rdoc 6.17.0 → 7.0.0

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.
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Aliki Search Implementation
3
+ *
4
+ * Search algorithm with the following priorities:
5
+ * 1. Exact full_name match always wins (for namespace/method queries)
6
+ * 2. Exact name match gets high priority
7
+ * 3. Match types:
8
+ * - Namespace queries (::) and method queries (# or .) match against full_name
9
+ * - Regular queries match against unqualified name
10
+ * - Prefix (10000) > substring (5000) > fuzzy (1000)
11
+ * 4. First character determines type priority:
12
+ * - Starts with lowercase: methods first
13
+ * - Starts with uppercase: classes/modules/constants first
14
+ * 5. Within same type priority:
15
+ * - Unqualified match > qualified match
16
+ * - Shorter name > longer name
17
+ * 6. Class methods > instance methods
18
+ * 7. Result limit: 30
19
+ * 8. Minimum query length: 1 character
20
+ */
21
+
22
+ var MAX_RESULTS = 30;
23
+ var MIN_QUERY_LENGTH = 1;
24
+
25
+ /*
26
+ * Scoring constants - organized in tiers where each tier dominates lower tiers.
27
+ * This ensures match type always beats type priority, etc.
28
+ *
29
+ * Tier 0: Exact matches (immediate return)
30
+ * Tier 1: Match type (prefix > substring > fuzzy)
31
+ * Tier 2: Exact name bonus
32
+ * Tier 3: Type priority (method vs class based on query case)
33
+ * Tier 4: Minor bonuses (top-level, class method, name length)
34
+ */
35
+ var SCORE_EXACT_FULL_NAME = 1000000; // Tier 0: Query exactly matches full_name
36
+ var SCORE_MATCH_PREFIX = 10000; // Tier 1: Query is prefix of name
37
+ var SCORE_MATCH_SUBSTRING = 5000; // Tier 1: Query is substring of name
38
+ var SCORE_MATCH_FUZZY = 1000; // Tier 1: Query chars appear in order
39
+ var SCORE_EXACT_NAME = 500; // Tier 2: Name exactly equals query
40
+ var SCORE_TYPE_PRIORITY = 100; // Tier 3: Preferred type (method/class)
41
+ var SCORE_TOP_LEVEL = 50; // Tier 4: Top-level over namespaced
42
+ var SCORE_CLASS_METHOD = 10; // Tier 4: Class method over instance method
43
+
44
+ /**
45
+ * Check if all characters in query appear in order in target
46
+ * e.g., "addalias" fuzzy matches "add_foo_alias"
47
+ */
48
+ function fuzzyMatch(target, query) {
49
+ var ti = 0;
50
+ for (var qi = 0; qi < query.length; qi++) {
51
+ ti = target.indexOf(query[qi], ti);
52
+ if (ti === -1) return false;
53
+ ti++;
54
+ }
55
+ return true;
56
+ }
57
+
58
+ /**
59
+ * Parse and normalize a search query
60
+ * @param {string} query - The raw search query
61
+ * @returns {Object} Parsed query with normalized form and flags
62
+ */
63
+ function parseQuery(query) {
64
+ // Lowercase for case-insensitive matching (so "hash" finds both Hash class and #hash methods)
65
+ var normalized = query.toLowerCase();
66
+ var isNamespaceQuery = query.includes('::');
67
+ var isMethodQuery = query.includes('#') || query.includes('.');
68
+
69
+ // Normalize . to :: (RDoc uses :: for class methods in full_name)
70
+ if (query.includes('.')) {
71
+ normalized = normalized.replace(/\./g, '::');
72
+ }
73
+
74
+ return {
75
+ original: query,
76
+ normalized: normalized,
77
+ isNamespaceQuery: isNamespaceQuery,
78
+ isMethodQuery: isMethodQuery,
79
+ // Namespace and method queries match against full_name instead of name
80
+ matchesFullName: isNamespaceQuery || isMethodQuery,
81
+ // If query starts with lowercase, prioritize methods; otherwise prioritize classes/modules/constants
82
+ prioritizeMethod: !/^[A-Z]/.test(query)
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Main search function
88
+ * @param {string} query - The search query
89
+ * @param {Array} index - The search index to search in
90
+ * @returns {Array} Array of matching entries, sorted by relevance
91
+ */
92
+ function search(query, index) {
93
+ if (!query || query.length < MIN_QUERY_LENGTH) {
94
+ return [];
95
+ }
96
+
97
+ var q = parseQuery(query);
98
+ var results = [];
99
+
100
+ for (var i = 0; i < index.length; i++) {
101
+ var entry = index[i];
102
+ var score = computeScore(entry, q);
103
+
104
+ if (score !== null) {
105
+ results.push({ entry: entry, score: score });
106
+ }
107
+ }
108
+
109
+ results.sort(function(a, b) {
110
+ return b.score - a.score;
111
+ });
112
+
113
+ return results.slice(0, MAX_RESULTS).map(function(r) {
114
+ return r.entry;
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Compute the relevance score for an entry
120
+ * @param {Object} entry - The search index entry
121
+ * @param {Object} q - Parsed query from parseQuery()
122
+ * @returns {number|null} Score or null if no match
123
+ */
124
+ function computeScore(entry, q) {
125
+ var name = entry.name;
126
+ var fullName = entry.full_name;
127
+ var type = entry.type;
128
+
129
+ var nameLower = name.toLowerCase();
130
+ var fullNameLower = fullName.toLowerCase();
131
+
132
+ // Exact full_name match (e.g., "Array#filter" matches Array#filter)
133
+ if (q.matchesFullName && fullNameLower === q.normalized) {
134
+ return SCORE_EXACT_FULL_NAME;
135
+ }
136
+
137
+ var matchScore = 0;
138
+ var target = q.matchesFullName ? fullNameLower : nameLower;
139
+
140
+ if (target.startsWith(q.normalized)) {
141
+ matchScore = SCORE_MATCH_PREFIX; // Prefix (e.g., "Arr" matches "Array")
142
+ } else if (target.includes(q.normalized)) {
143
+ matchScore = SCORE_MATCH_SUBSTRING; // Substring (e.g., "ray" matches "Array")
144
+ } else if (fuzzyMatch(target, q.normalized)) {
145
+ matchScore = SCORE_MATCH_FUZZY; // Fuzzy (e.g., "addalias" matches "add_foo_alias")
146
+ } else {
147
+ return null;
148
+ }
149
+
150
+ var score = matchScore;
151
+ var isMethod = (type === 'instance_method' || type === 'class_method');
152
+
153
+ if (q.prioritizeMethod ? isMethod : !isMethod) {
154
+ score += SCORE_TYPE_PRIORITY;
155
+ }
156
+
157
+ if (type === 'class_method') score += SCORE_CLASS_METHOD;
158
+ if (name === fullName) score += SCORE_TOP_LEVEL; // Top-level (Hash) > namespaced (Foo::Hash)
159
+ if (nameLower === q.normalized) score += SCORE_EXACT_NAME; // Exact name match
160
+ score -= name.length;
161
+
162
+ return score;
163
+ }
164
+
165
+ /**
166
+ * SearchRanker class for compatibility with the Search UI
167
+ * Provides ready() and find() interface
168
+ */
169
+ function SearchRanker(index) {
170
+ this.index = index;
171
+ this.handlers = [];
172
+ }
173
+
174
+ SearchRanker.prototype.ready = function(fn) {
175
+ this.handlers.push(fn);
176
+ };
177
+
178
+ SearchRanker.prototype.find = function(query) {
179
+ var q = parseQuery(query);
180
+ var rawResults = search(query, this.index);
181
+ var results = rawResults.map(function(entry) {
182
+ return formatResult(entry, q);
183
+ });
184
+
185
+ var _this = this;
186
+ this.handlers.forEach(function(fn) {
187
+ fn.call(_this, results, true);
188
+ });
189
+ };
190
+
191
+ /**
192
+ * Format a search result entry for display
193
+ */
194
+ function formatResult(entry, q) {
195
+ var result = {
196
+ title: highlightMatch(entry.full_name, q),
197
+ path: entry.path,
198
+ type: entry.type
199
+ };
200
+
201
+ if (entry.snippet) {
202
+ result.snippet = entry.snippet;
203
+ }
204
+
205
+ return result;
206
+ }
207
+
208
+ /**
209
+ * Add highlight markers (\u0001 and \u0002) to matching portions of text
210
+ * @param {string} text - The text to highlight
211
+ * @param {Object} q - Parsed query from parseQuery()
212
+ */
213
+ function highlightMatch(text, q) {
214
+ if (!text || !q) return text;
215
+
216
+ var textLower = text.toLowerCase();
217
+ var query = q.normalized;
218
+
219
+ // Try contiguous match first (prefix or substring)
220
+ var matchIndex = textLower.indexOf(query);
221
+ if (matchIndex !== -1) {
222
+ return text.substring(0, matchIndex) +
223
+ '\u0001' + text.substring(matchIndex, matchIndex + query.length) + '\u0002' +
224
+ text.substring(matchIndex + query.length);
225
+ }
226
+
227
+ // Fall back to fuzzy highlight (highlight each matched character)
228
+ var result = '';
229
+ var ti = 0;
230
+ for (var qi = 0; qi < query.length; qi++) {
231
+ var charIndex = textLower.indexOf(query[qi], ti);
232
+ if (charIndex === -1) return text;
233
+ result += text.substring(ti, charIndex);
234
+ result += '\u0001' + text[charIndex] + '\u0002';
235
+ ti = charIndex + 1;
236
+ }
237
+ result += text.substring(ti);
238
+ return result;
239
+ }
@@ -163,15 +163,13 @@
163
163
  <summary>Source</summary>
164
164
  </details>
165
165
  </div>
166
+ <div class="method-source-code" id="<%= method.html_name %>-source">
167
+ <pre><%= method.markup_code %></pre>
168
+ </div>
166
169
  <%- end %>
167
170
 
168
171
  <%- unless method.skip_description? then %>
169
172
  <div class="method-description">
170
- <%- if method.token_stream then %>
171
- <div class="method-source-code" id="<%= method.html_name %>-source">
172
- <pre><%= method.markup_code %></pre>
173
- </div>
174
- <%- end %>
175
173
  <%- if method.mixin_from then %>
176
174
  <div class="mixin-from">
177
175
  <%= method.singleton ? "Extended" : "Included" %> from <a href="<%= klass.aref_to(method.mixin_from.path) %>"><%= method.mixin_from.full_name %></a>
data/lib/rdoc/options.rb CHANGED
@@ -411,7 +411,7 @@ class RDoc::Options
411
411
  @files = nil
412
412
  @force_output = false
413
413
  @force_update = true
414
- @generator_name = "darkfish"
414
+ @generator_name = "aliki"
415
415
  @generators = RDoc::RDoc::GENERATORS
416
416
  @generator_options = []
417
417
  @hyperlink_all = false
@@ -118,7 +118,7 @@ class RDoc::RubyGemsHook
118
118
  end
119
119
 
120
120
  ##
121
- # Generates documentation using the named +generator+ ("darkfish" or "ri")
121
+ # Generates documentation using the named +generator+ ("aliki" or "ri")
122
122
  # and following the given +options+.
123
123
  #
124
124
  # Documentation will be generated into +destination+
@@ -190,7 +190,7 @@ class RDoc::RubyGemsHook
190
190
 
191
191
  Dir.chdir @spec.full_gem_path do
192
192
  # RDoc::Options#finish must be called before parse_files.
193
- # RDoc::Options#finish is also called after ri/darkfish generator setup.
193
+ # RDoc::Options#finish is also called after ri/aliki generator setup.
194
194
  # We need to dup the options to avoid modifying it after finish is called.
195
195
  parse_options = options.dup
196
196
  parse_options.finish
@@ -202,7 +202,7 @@ class RDoc::RubyGemsHook
202
202
  document 'ri', options, @ri_dir if
203
203
  @generate_ri and (@force or not File.exist? @ri_dir)
204
204
 
205
- document 'darkfish', options, @rdoc_dir if
205
+ document 'aliki', options, @rdoc_dir if
206
206
  @generate_rdoc and (@force or not File.exist? @rdoc_dir)
207
207
  end
208
208
 
data/lib/rdoc/version.rb CHANGED
@@ -5,6 +5,6 @@ module RDoc
5
5
  ##
6
6
  # RDoc version you are using
7
7
 
8
- VERSION = '6.17.0'
8
+ VERSION = '7.0.0'
9
9
 
10
10
  end
data/rdoc.gemspec CHANGED
@@ -38,7 +38,7 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat
38
38
  # for ruby core repository. It was generated by
39
39
  # `git ls-files -z`.split("\x0").each {|f| puts " #{f.dump}," unless f.start_with?(*%W[test/ spec/ features/ .]) }
40
40
  non_lib_files = [
41
- "CONTRIBUTING.rdoc",
41
+ "CONTRIBUTING.md",
42
42
  "CVE-2013-0256.rdoc",
43
43
  "ExampleMarkdown.md",
44
44
  "ExampleRDoc.rdoc",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rdoc
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.17.0
4
+ version: 7.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Hodel
@@ -13,7 +13,7 @@ authors:
13
13
  - ITOYANAGI Sakura
14
14
  bindir: exe
15
15
  cert_chain: []
16
- date: 2025-12-07 00:00:00.000000000 Z
16
+ date: 2025-12-18 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: psych
@@ -73,7 +73,7 @@ executables:
73
73
  - ri
74
74
  extensions: []
75
75
  extra_rdoc_files:
76
- - CONTRIBUTING.rdoc
76
+ - CONTRIBUTING.md
77
77
  - CVE-2013-0256.rdoc
78
78
  - ExampleMarkdown.md
79
79
  - ExampleRDoc.rdoc
@@ -84,7 +84,7 @@ extra_rdoc_files:
84
84
  - RI.md
85
85
  - TODO.rdoc
86
86
  files:
87
- - CONTRIBUTING.rdoc
87
+ - CONTRIBUTING.md
88
88
  - CVE-2013-0256.rdoc
89
89
  - ExampleMarkdown.md
90
90
  - ExampleRDoc.rdoc
@@ -153,7 +153,9 @@ files:
153
153
  - lib/rdoc/generator/template/aliki/index.rhtml
154
154
  - lib/rdoc/generator/template/aliki/js/aliki.js
155
155
  - lib/rdoc/generator/template/aliki/js/c_highlighter.js
156
- - lib/rdoc/generator/template/aliki/js/search.js
156
+ - lib/rdoc/generator/template/aliki/js/search_controller.js
157
+ - lib/rdoc/generator/template/aliki/js/search_navigation.js
158
+ - lib/rdoc/generator/template/aliki/js/search_ranker.js
157
159
  - lib/rdoc/generator/template/aliki/js/theme-toggle.js
158
160
  - lib/rdoc/generator/template/aliki/page.rhtml
159
161
  - lib/rdoc/generator/template/aliki/servlet_not_found.rhtml
@@ -322,7 +324,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
322
324
  - !ruby/object:Gem::Version
323
325
  version: '2.2'
324
326
  requirements: []
325
- rubygems_version: 3.6.7
327
+ rubygems_version: 3.6.9
326
328
  specification_version: 4
327
329
  summary: RDoc produces HTML and command-line documentation for Ruby projects
328
330
  test_files: []
data/CONTRIBUTING.rdoc DELETED
@@ -1,219 +0,0 @@
1
- = Developer Introduction
2
-
3
- So you want to write a generator, fix a bug, or otherwise work with RDoc. This
4
- document provides an overview of how RDoc works from parsing options to
5
- generating output. Most of the documentation can be found in the specific
6
- classes for each feature.
7
-
8
- == Bugs
9
-
10
- If you think you found a bug, file a ticket on the {issues
11
- tracker}[https://github.com/ruby/rdoc/issues] on github.
12
-
13
- If your bug involves an error RDoc produced please include a sample file that
14
- illustrates the problem or link to the repository or gem that is associated
15
- with the bug.
16
-
17
- Please include steps to reproduce the issue. Here are some examples of good
18
- issues:
19
-
20
- * https://github.com/ruby/rdoc/issues/55
21
- * https://github.com/ruby/rdoc/issues/61
22
-
23
- == Developer Quick Start
24
-
25
- RDoc uses bundler for development. To get ready to work on RDoc run:
26
-
27
- $ gem install bundler
28
- [...]
29
- $ bundle install
30
- [...]
31
- $ rake
32
- [...]
33
-
34
- This will install all the necessary dependencies for development with rake,
35
- generate documentation and run the tests for the first time.
36
-
37
- If the tests don't pass on the first run check the {GitHub Actions page}[https://github.com/ruby/rdoc/actions] to see if there are any known failures
38
- (there shouldn't be).
39
-
40
- You can now use `rake` and `autotest` to run the tests.
41
-
42
- Note: the `rake` command must be used first before running any tests, because
43
- it's used to generate various parsers implemented in RDoc. Also `rake clean` is
44
- helpful to delete these generated files.
45
-
46
- == Glossary
47
-
48
- Here are definitions for some common terms in the RDoc documentation. The
49
- list also briefly describes how the components of RDoc interact.
50
-
51
- parser::
52
- Parses files and creates a documentation tree from the contents.
53
-
54
- documentation tree::
55
- The documentation tree represents files, classes, modules, methods,
56
- constants, includes, comments and other ruby syntax features as a tree.
57
- RDoc walks this tree with a generator to create documentation.
58
-
59
- generator::
60
- Walks the documentation tree and generates output.
61
-
62
- RDoc ships with two generators, the Darkfish generator creates HTML and the
63
- RI generator creates an RI data store.
64
-
65
- markup parser::
66
- Parses comments from a file into a generic markup tree.
67
-
68
- The markup parsers allow RDoc to handle RDoc, TomDoc, rd and Markdown format
69
- documentation with common formatters.
70
-
71
- markup tree::
72
- Each parsed comment has a markup tree that represents common markup items
73
- such as headings, paragraphs, lists or verbatim text sections for example
74
- code or output.
75
-
76
- A generator uses a formatters to walks the tree to create output. Some
77
- generators use multiple formatters on a markup tree to produce the output.
78
-
79
- formatter::
80
- Converts a parsed markup tree into some form other form of markup.
81
-
82
- Formatters can either produce a one-to-one conversion, such as ToHtml, or
83
- extract part of the parsed result, such as ToHtmlSnippet which outputs the
84
- first 100 characters as HTML.
85
-
86
- == Plugins
87
-
88
- When 'rdoc/rdoc' is loaded RDoc looks for 'rdoc/discover' files in your
89
- installed gems. This can be used to load parsers, alternate generators, or
90
- additional preprocessor directives. An rdoc plugin layout should look
91
- something like this:
92
-
93
- lib/rdoc/discover.rb
94
- lib/my/rdoc/plugin.rb
95
- # etc.
96
-
97
- In your rdoc/discover.rb file you will want to wrap the loading of your plugin
98
- in an RDoc version check like this:
99
-
100
- begin
101
- gem 'rdoc', '~> 3'
102
- require 'my/rdoc/plugin'
103
- rescue Gem::LoadError
104
- end
105
-
106
- === Plugin Types
107
-
108
- In RDoc you can change the following behaviors:
109
-
110
- * Add a parser for a new file format
111
- * Add a new output generator
112
- * Add a new markup directive
113
- * Add a new type of documentation markup
114
- * Add a new type of formatter
115
-
116
- All of these are described below
117
-
118
- == Option Parsing
119
-
120
- Option parsing is handled by RDoc::Options. When you're writing a generator
121
- you can provide the user with extra options by providing a class method
122
- +setup_options+. The option parser will call this after your generator is
123
- loaded. See RDoc::Generator for details.
124
-
125
- == File Parsing
126
-
127
- After options are parsed, RDoc parses files from the files and directories in
128
- ARGV. RDoc compares the filename against what each parser claims it can parse
129
- via RDoc::Parser#parse_files_matching. For example, RDoc::Parser::C can parse
130
- C files, C headers, C++ files, C++ headers and yacc grammars.
131
-
132
- Once a matching parser class is found it is instantiated and +scan+ is called.
133
- The parser needs to extract documentation from the file and add it to the RDoc
134
- document tree. Usually this involves starting at the root and adding a class
135
- or a module (RDoc::TopLevel#add_class and RDoc::TopLevel#add_module) and
136
- proceeding to add classes, modules and methods to each nested item.
137
-
138
- When the parsers are finished the document tree is cleaned up to remove
139
- dangling references to aliases and includes that were not found (and may exist
140
- in a separate library) through RDoc::ClassModule#complete.
141
-
142
- To write your own parser for a new file format see RDoc::Parser.
143
-
144
- === Documentation Tree
145
-
146
- The parsers build a documentation tree that is composed of RDoc::CodeObject and
147
- its subclasses. There are various methods to walk the tree to extract
148
- information, see RDoc::Context and its subclasses.
149
-
150
- Within a class or module, attributes, methods and constants are divided into
151
- sections. The section represents a functional grouping of parts of the class.
152
- TomDoc uses the sections "Public", "Internal" and "Deprecated". The sections
153
- can be enumerated using RDoc::Context#each_section.
154
-
155
- == Output Generation
156
-
157
- An RDoc generator turns the documentation tree into some other kind of output.
158
- RDoc comes with an HTML generator (RDoc::Generator::Darkfish) and an RI
159
- database generator (RDoc::Generator::RI). The output a generator creates does
160
- not have to be human-readable.
161
-
162
- To create your own generator see RDoc::Generator.
163
-
164
- === Comments
165
-
166
- In RDoc 3.10 and newer the comment on an RDoc::CodeObject is now an
167
- RDoc::Comment object instead of a String. This is to support various
168
- documentation markup formats like rdoc, TomDoc and rd. The comments are
169
- normalized to remove comment markers and remove indentation then parsed lazily
170
- via RDoc::Comment#document to create a generic markup tree that can be
171
- processed by a formatter.
172
-
173
- To add your own markup format see RDoc::Markup@Other+directives
174
-
175
- ==== Formatters
176
-
177
- To transform a comment into some form of output an RDoc::Markup::Formatter
178
- subclass is used like RDoc::Markup::ToHtml. A formatter is a visitor that
179
- walks a parsed comment tree (an RDoc::Markup::Document) of any format. To help
180
- write a formatter RDoc::Markup::FormatterTestCase exists for generic parsers,
181
- and RDoc::Markup::TextFormatterTestCase which contains extra test cases for
182
- text-type output (like +ri+ output).
183
-
184
- RDoc ships with formatters that will turn a comment into HTML, rdoc-markup-like
185
- text, ANSI or terminal backspace highlighted text, HTML, cross-referenced HTML,
186
- an HTML snippet free of most markup, an HTML label for use in id attributes, a
187
- table-of-contents page, and text with only code blocks.
188
-
189
- The output of the formatter does not need to be text or text-like.
190
- RDoc::Markup::ToLabel creates an HTML-safe label for use in an HTML id
191
- attribute. A formatter could count the number of words and the average word
192
- length for a comment, for example.
193
-
194
- ==== Directives
195
-
196
- For comments in markup you can add new directives (:nodoc: is a directive).
197
- Directives may replace text or store it off for later use.
198
-
199
- See RDoc::Markup::PreProcess::register for details.
200
-
201
- === JSONIndex
202
-
203
- RDoc contains a special generator, RDoc::Generator::JSONIndex, which creates a
204
- JSON-based search index and includes a search engine for use with HTML output.
205
- This generator can be used to add searching to any HTML output and is designed
206
- to be called from inside an HTML generator.
207
-
208
- == Markup
209
-
210
- Additional documentation markup formats can be added to RDoc. A markup
211
- parsing class must respond to \::parse and accept a String argument containing
212
- the markup format. An RDoc::Document containing documentation items
213
- (RDoc::Markup::Heading, RDoc::Markup::Paragraph, RDoc::Markup::Verbatim, etc.)
214
- must be returned.
215
-
216
- To register the parser with rdoc, add the markup type's name and class to the
217
- RDoc::Text::MARKUP_FORMAT hash like:
218
-
219
- RDoc::Text::MARKUP_FORMAT['rdoc'] = RDoc::Markup