1 Commits
Author SHA1 Message Date
wkh c24d543e8e Handbuch als Markdown: doc/handbuch.md aus dem OCR-Text, mit Konverter 2026-10-30 10:17:34 +01:00
4 changed files with 20562 additions and 0 deletions
+5
View File
@@ -5,6 +5,11 @@ Alle wichtigen Änderungen stehen hier. Das Format folgt
folgen [Semantic Versioning](https://semver.org/lang/de/). Die Version steht in
`lib/rdb/version.rb`; `rdb --version` zeigt sie an.
## [Unveröffentlicht]
### Hinzugefügt
- `doc/handbuch.md`: das Handbuch als lesbares Markdown (Abschnitte, Überschriften, Absätze, Codeblöcke, Listen, Seitenmarken mit Anker je PDF-Seite), erzeugt mit `tools/handbuch_md.rb` aus `doc/handbuch.txt`. Die OCR-Fehler bleiben; die `.txt` bleibt die Quelle.
## [0.8.0] - 2026-10-30
**Beim Umstieg:** Hashindizes (`.h`) und mit `index -x` gebaute Indizes (`.i`) neu bauen.
+7
View File
@@ -97,6 +97,12 @@ Er ist nicht korrigiert: Tabulatoren fehlen, Tabellen können verrutscht sein. J
Seite ist mit PDF- und Buchseite markiert (Buchseite = PDF-Seite − 10). Kapitel 1–16
sind ein Lehrbuch, Kapitel 17 enthält dieselben Man-Pages wie `man/`.
`doc/handbuch.md` ist dieselbe Vorlage lesbar gesetzt: ein Abschnitt je „Section“, Überschriften,
zusammengefügte Absätze, Befehle und Tabellenausgaben in Codeblöcken, Listen, und vor jeder Seite
eine Marke mit PDF- und Buchseite (Anker `#s27` springt zur PDF-Seite 27). Die Datei entsteht mit
`ruby tools/handbuch_md.rb` aus der `.txt`; die Umsetzung ist heuristisch, die OCR-Fehler bleiben,
und im Zweifel gilt der Scan.
`demo/handbuch/` enthält die Beispiele aus dem Lehrbuch mit ihren Tabellen, ein
Unterordner je Kapitel. `beispiele.txt` hält zu jedem Beispiel den Befehl und die
Ausgabe, wie sie im Buch steht; `test/handbuch_test.rb` führt alles aus und vergleicht
@@ -296,6 +302,7 @@ man/ Man-Pages (troff-Quellen aus den MWC-Archiven)
demo/ Beispieltabellen; testall.cmds = je ein Beispiel pro Befehl, testall.old = letzter Lauf
demo/handbuch/ die Beispiele aus dem gedruckten Handbuch, je Kapitel ein Ordner
doc/handbuch.txt Text des gedruckten Handbuchs (OCR)
doc/handbuch.md derselbe Text als Markdown, erzeugt mit tools/handbuch_md.rb
test/ Tests
```
+20419
View File
File diff suppressed because it is too large Load Diff
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# Turns doc/handbuch.txt (OCR, one run of text per page) into doc/handbuch.md:
# one section per book section, running page headers dropped, wrapped lines
# joined into paragraphs, commands and table output in code blocks, and a
# marker with PDF and book page before each page's text.
#
# ruby tools/handbuch_md.rb [in.txt [out.md]]
#
# The OCR is not corrected. The conversion is heuristic (see prose?/heading?),
# so doc/handbuch.txt stays the source and the scan is the final authority.
SRC = ARGV[0] || File.expand_path('../doc/handbuch.txt', __dir__)
DST = ARGV[1] || File.expand_path('../doc/handbuch.md', __dir__)
PAGE = /\A==== PDF-Seite (\d+)(?: \/ Buchseite (\d+))? ====\z/
SHELLISH = %r{[|<>$=\[\]{}\\~`]|^\s*[#/]|\b(?:echo|cat|cd|sh|awk|sed|sort|ls|exec|read|set|if|then|fi|done|do|for|while|case|esac)\b\s}
SECTION = /\ASection (\d+|[A-Z]):\z/
# A book page starts with a running header that carries the book page number:
# "90 Shell Programming" on even pages, "Shell Programming 91" on odd ones.
def header?(line, book)
book && line.size <= 70 && (line.match?(/\A#{book}[ _]+\S/) || line.match?(/\S[ _]+#{book}\z/))
end
# Page number and tab label ("TUTORIAL", "LEXICON") at the foot of a page.
def footer?(block, book)
block.size == 1 && (block[0] == book.to_s || %w[TUTORIAL LEXICON].include?(block[0]))
end
# Prose goes through Markdown: keep its * _ < > from being read as markup.
def escape(text)
text.gsub(/([*_<>`\\])/, '\\\\\1')
end
def prose?(lines)
return false if lines.size == 1
longest = lines.map(&:size).max
return true if longest >= 80 && lines.count { |l| l.match?(SHELLISH) } * 2 < lines.size
lines.first.size >= 70 && lines.none? { |l| l.match?(/\A\s*[$#]|\|\s*\w+/) } && lines.last.match?(/[a-z.,;:)]\z/)
end
# One line that looks like a command or a name from a listing, not a sentence.
def command?(line)
(line.match?(SHELLISH) && line.split.size < 6) || (line.match?(/\A[a-z]/) && line.size <= 40 && line !~ /[.!?:]\z/)
end
def heading?(lines, following)
return false unless lines.size == 1
l = lines[0]
return false if l.size > 70 || l.size < 4 || l.match?(/[.,;]\z/) || l !~ /\A[A-Z0-9]/
return false if l.match?(SHELLISH) || l.count(' ') < 1 || l.count(' ') > 9
return false unless following && prose?(following)
l.scan(/\b[A-Za-z][a-z]*\b/).first(6).count { |w| w.match?(/\A[A-Z]/) || %w[and or of the to in for a an with on from by is as at].include?(w) } >= [l.split.size - 1, 1].max
end
def join(lines)
lines.each_with_object(+'') do |l, out|
if out.empty? then out << l
elsif out.match?(/[a-z]-\z/) && l.match?(/\A[a-z]/) then out.chop!; out << l
else out << ' ' << l
end
end
end
pages = []
cur = nil
File.readlines(SRC, chomp: true).each do |line|
if (m = line.match(PAGE))
cur = { pdf: m[1].to_i, book: m[2]&.to_i, lines: [] }
pages << cur
elsif cur
cur[:lines] << line.rstrip
end
end
out = +"# COHERENT /rdb Relational Database Manager\n\n"
out << "*Mark Williams Company, 1992.* Text per OCR (tesseract) aus dem Scan, nicht korrigiert: Tabulatoren fehlen, Tabellen und Zeichen können falsch sein. ".dup
out << "Im Zweifel gilt der Scan. Dieses Dokument entsteht aus `doc/handbuch.txt` mit `tools/handbuch_md.rb`; Änderungen gehören dorthin oder in den Konverter.\n\n"
pages.each do |pg|
lines = pg[:lines].dup
label = pg[:book] ? "PDF-Seite #{pg[:pdf]} · Buchseite #{pg[:book]}" : "PDF-Seite #{pg[:pdf]}"
out << %(<a id="s#{pg[:pdf]}"></a>\n*— #{label} —*\n\n)
# Front matter (cover, copyright, contents): keep as it is, in a block.
if pg[:book].nil?
text = lines.join("\n").strip
out << "```text\n#{text}\n```\n\n" unless text.empty?
next
end
lines.shift while lines.first&.strip&.empty?
lines.shift if lines.first && header?(lines.first, pg[:book])
blocks = lines.slice_when { |a, b| a.strip.empty? != b.strip.empty? }.reject { |b| b.first.strip.empty? }
blocks.reject! { |b| footer?(b.map(&:strip), pg[:book]) }
blocks.each_with_index do |b, i|
b = b.map(&:strip)
nxt = blocks[i + 1]&.map(&:strip)
if b[0].match?(SECTION) && b.size <= 2
title = b[1] || (nxt&.size == 1 ? nxt.first : nil)
blocks[i + 1] = [''] if b.size == 1 && title # consumed
out << "# #{b[0].sub(/:\z/, '')}#{" — #{title}" if title}\n\n"
elsif b == ['']
next
elsif heading?(b, nxt)
out << "## #{b[0]}\n\n"
elsif prose?(b) || (b.size == 1 && !command?(b[0]))
# list items inside a paragraph: "1. ..." or an OCR bullet (* ° •)
item = /\A(\d+\.|[*°•-])\s+/
b.slice_before { |l| l.match?(item) }.each do |part|
if part.first.match?(item)
out << part.then { |l| join(l) }.sub(item) { Regexp.last_match(1).match?(/\d/) ? Regexp.last_match(0) : '- ' }.then { |t| escape(t) } << "\n"
else
out << "\n" << escape(join(part)) << "\n\n"
end
end
out << "\n"
else
out << "```text\n#{b.join("\n")}\n```\n\n"
end
end
end
File.write(DST, out.gsub(/\n{3,}/, "\n\n"))
puts "#{DST}: #{pages.size} Seiten, #{out.lines.size} Zeilen"