Dec. 20th, 2023

henry_flower: A melancholy wolf (Default)

Як ви пишете 25м3? Ув маркдауні це виглядає рівно так, як можна очікувати:

25м<sup>3</sup>

Деякі редактори (імакс) дозволяють друкувати superscript цифри; наприклад для надрядкового 2 треба віртуозно натиснути C-x 8 ^ 2. Надрядкову a так надрукувати не вийде--замість неї з'явиться літера â.

Юнікод мав би мати надрядкові та підрядкові симболи для кожної абетки, але цього не сталося. Присутній сабсет--для, наприклад, латини та безнадійної кирилиці--розмазан по випадкових місцях. Якщо підрядкові цифри можна отримати знаючи офсета, то для абеток це працювати не буде: e.g., підрядкова літера j сидить ув секції Latin Extended-C, втиснута поміж загадковою фінно-угорською (яка дуже нагадує мотематичний квантор існування) та надрядковою великою V.

Юнікод ніби має спеціяльну секцію Superscript and Subscripts, але вона виглядає як поганий жарт:

Superscript and Subscripts

(Пусті світлі клітинки це не помилка рендерінґу симбола, а "not assigned" місця.)

Врешті-решт, якщо ретельно пошукати, знайти локацію більшості (не всіх) над/підрядкових літер можна, і тоді написавши елементарний скрипт що робить заміну звичайних літер на над/підрядкові, будь-який пристойний редактор дозволить замінити виділений текста на результат роботи скрипта.

Це є звичайно, цікаво, але було би цікавіше виділяти маркапний варіянт де трансформувалися би лише елементи <sup> чи <sub>, наприклад для хформули y = x2a:

Працює це так. 1й скрипт робить заміну відповідних симболів:

$ lsb_release -d | ./supsub sub
dₑₛ𞁞ᵣᵢₚₜᵢₒₙ:    fₑdₒᵣₐ ᵣₑₗₑₐₛₑ ₃₉ ₍ₜₕᵢᵣₜᵧ ₙᵢₙₑ₎
$ lsb_release -d | ./supsub sub | ./supsub invert
ᵈ𞀵ˢ𞀿ʳⁱ𞀾ᵗⁱ𞀼ⁿ:    ᶠ𞀵ᵈ𞀼ʳ𞀰 ʳ𞀵ˡ𞀵𞀰ˢ𞀵 ³⁹ ⁽ᵗʰⁱʳᵗʸ ⁿⁱⁿ𞀵⁾
$ lsb_release -d | ./supsub sub | ./supsub invert | ./supsub restore
dеsсrірtіоn:    fеdоrа rеlеаsе 39 (thіrty nіnе)

(Рендерінґ бовзерами бажає залишати кращого.)

$ cat supsub
#!/usr/bin/env -S ruby --disable-gems

db = DATA.read.split(/\s+/).filter {|v| v}
$supa = db.map {|v| [v[0], v[1]] }.to_h
$sub = db.map {|v| [v[0], v[2]] }.to_h

mode = 'sup|sub|invert|restore'
abort "Usage: supsub #{mode} < file.txt" unless ARGV[0] =~ /^#{mode}$/

def tr mode, chars
  case mode
  when 'sup'
    chars.map { |ch| $supa[ch.downcase] || ch }.join
  when 'sub'
    chars.map { |ch| $sub[ch.downcase] || ch }.join
  when 'invert'
    supa_v = $supa.invert
    sub_v = $sub.invert
    chars.map do |ch|
      if supa_v[ch]
        $sub[supa_v[ch]] || ch
      elsif sub_v[ch]
        $supa[sub_v[ch]] || ch
      else
        ch
      end
    end.join
  else # restore
    supa_v = $supa.invert
    sub_v = $sub.invert
    chars.map { |ch| supa_v[ch] || sub_v[ch] || ch}.join
  end
end

while (line = STDIN.gets)
  print tr(ARGV[0], line.chars)
end

__END__
0⁰₀ 1¹₁ 2²₂ 3³₃ 4⁴₄ 5⁵₅ 6⁶₆ 7⁷₇ 8⁸₈ 9⁹₉ +⁺₊ -⁻₋ =⁼₌ (⁽₍ )⁾₎
aᵃₐ bᵇb cᶜ𞁞 dᵈd eᵉₑ fᶠf gᵍg hʰₕ iⁱᵢ jʲⱼ kᵏₖ lˡₗ mᵐₘ nⁿₙ oᵒₒ
pᵖₚ q𐞥q rʳᵣ sˢₛ tᵗₜ uᵘᵤ vᵛᵥ wʷw xˣₓ yʸᵧ zᶻz
а𞀰ₐ б𞀱𞁒 в𞀲𞁓 г𞀳𞁔 ґґ𞁧 д𞀴𞁕 е𞀵ₑ ж𞀶𞁗 з𞀷𞁘 и𞀸𞁙 іⁱ𞁨 їїї ййй
к𞀹𞁚 л𞀺𞁛 м𞀻ₘ нᵸн о𞀼ₒ п𞀽𞁝 р𞀾ₚ с𞀿𞁞 т𞁀т у𞁁𞁟 ф𞁂𞁠 х𞁃𞁡 ц𞁄𞁢
ч𞁅𞁣 ш𞁆𞁤 щщщ ьꚝь ю𞁉ю яяя

Замінювати воно може лише цифри, симболи латини та української абетки. Сили на пошук голок ув юнікоді у мене закінчилися.

2й скрипта є трохи цікавіший:

$ echo '<sub>test</sub> <i>lol</i> <sup>haha</sup> but <sup>it works</sup>!' | ./supsub-xml
ₜₑₛₜ <i>lol</i> ʰᵃʰᵃ but ⁱᵗ ʷᵒʳᵏˢ!

За допомогою 鋸 парситься фрагменти xml та викликається попередній скрипта (supsub) коли зустрічається елемент <sup> чи <sub>. Версія 1 скрипта supsub-xml може бути такою примітивною як

#!/usr/bin/env ruby

require 'nokogiri'

cmd = ARGV[0] || File.join(__dir__, 'supsub')
doc = Nokogiri::HTML.fragment STDIN.read

doc.css('sup,sub').each do |node|
  IO.popen("#{cmd} #{node.name}", 'w+') do |t|
    t.write node.text
    t.close_write
    node.replace t.gets
  end
end

print doc.to_s

Воно працює, і, для більшості випадків його можна залишити як є, але форкання на кожну трансформацію виглядає варварським.

Стівенс в своїй APUE розповідає про coprocesses, які він описує як процесси які живуть одночасно з головною програмою (parent), і до яких parent під'єднується через 2 пайпи, які він сам створює:

Driving a coprocess

Bash підтримує коупроцеси, але використовують їх ~ніхто, тому що ⓐ нарід ув більшості не гребе шо то за коупроцеси такі, ⓑ пише майкросервіси на расті для калькуляції 6*8, ⓒ це є забута стародавня технологія предків.

Найпростіший приклад виглядає так: parent запускає звичайну утіліту tr(1), яка має робити upcase для всього, шо надходить на її stdin:

$ cat coprocess
#!/usr/bin/env ruby

parent_in, parent_out = IO.pipe
child_in, child_out = IO.pipe
spawn "tr '[a-z]' '[A-Z]'", in: child_in, out: parent_out
parent_out.close
child_in.close

child_out.puts "lol"
print parent_in.gets
child_out.puts "haha"
print parent_in.gets

На жаль, як запустити ./coprocess, ми не отримаємо LOL та HAHA: скрипта зависне ув дедлоку на рядку print parent_in.gets: tr з'їсть свій stdin і напише результат ув свій stdout, використовуючи fwrite(2) з libc (принаймі лайнаксна версія з coreutils), яка буферує результат, тому parent буде чекати на байти з пайпу вічно.

Зарадити цьому можна пустивши tr через утіліту stdbuf(1):

spawn "stdbuf -i0 -o0 tr '[a-z]' '[A-Z]'", in: child_in, out: parent_out

тоді дейта почне рухатися пайпами:

$ ./coprocess
LOL
HAHA

Цей хфінт вухами працює лише для хфункцій з libc. Рубі (як і інші мови) має свій IO механізм, тому stdbuf йому допоможе як дикобразу вогнемета.

Але вихід є: якщо змусити коупроцесс думати що той є підключений до терміналу, тоді спрощена схема роботи supsub-xml що форкає supsub буде виглядати на кшталт (NSFW):

coprocess via pty

Рубі має чудове вбудоване ікстеншона pty, яке дозволяє замість IO.pipe для parent писати PTY.open, не чіпаючи все інше.

$ cat supsub-xml
#!/usr/bin/env ruby

require 'nokogiri'
require 'pty'

cmd = ARGV[0] || File.join(__dir__, 'supsub')

class Coprocess
  def initialize cmd
    @master, slave = PTY.open
    read, @write = IO.pipe
    spawn cmd, in: read, out: slave
    read.close
    slave.close
  end

  def puts str; @write.puts str; end
  def gets; @master.gets; end
end

transforms = {
  "sup" => Coprocess.new("#{cmd} sup"),
  "sub" => Coprocess.new("#{cmd} sub")
}

doc = Nokogiri::HTML.fragment STDIN.read
doc.css('sup,sub').each do |node|
  tr = transforms[node.name]
  tr.puts node.text
  node.replace tr.gets.chomp
rescue
  warn "transforming `#{node.text}` failed: #{$!}"
end

print doc.to_s

Ув версії 2 ми створюємо 2 коупроцесси для елементів <sup> та <sub> заздалегідь і використовуємо їх як локальні майкросервіси. Якщо додати sleep ув кінець скрипта, можна подивитися як це виглядає фізично:

$ $$
bash: 42396: command not found
$ echo '<sub>test</sub> <i>lol</i> <sup>haha</sup>!' | ./supsub-xml
ₜₑₛₜ <i>lol</i> ʰᵃʰᵃ!

поки воно спить, з іншого терміналу:

$ pstree -p 42396 -al
bash,42396
  └─ruby,100032 ./supsub-xml
      ├─ruby,100033 --disable-gems ... sup
      └─ruby,100034 --disable-gems ... sub

$ file /proc/100032/fd/* # supsub-xml
...
$ file /proc/100033/fd/* # supsub sup
...

May 2025

M T W T F S S
   12 34
5678910 11
1213 1415 161718
19202122232425
262728293031 

Expand Cut Tags

No cut tags