\NeedsTeXFormat{LaTeX2e}
\ProvidesExplPackage{morse-simple}{2026-07-04}{1.0}{Morse code strings}

% ==============================
% Style file: Preliminary declarations

\RequirePackage{graphicx} % Needed for operations with dot
\RequirePackage{color} % Needed for \cwEnlargeDot and \cw\EnlargeDah


% By default, set the dit and dah symbols at the math axis height.

% See https://tex.stackexchange.com/a/129840/339
% #1: math style,
%     one of\displaystyle, \textstyle, \scriptstyle, \scriptscriptstyle
\newcommand*{\getMathAxisHgt}[1]{%
    \newsavebox{\cwbox}
    \savebox{\cwbox}{$#1\vcenter{}$}
    \the\ht\cwbox}

\newcommand{\mathAxisHgt}{\getMathAxisHgt{\textstyle}}


% Thickness of a dit or dah bar
% Comes first because height above baseline may adjust for thickness.
\dim_new:N \g__cw_charthick_dim
\dim_gset:Nn \g__cw_charthick_dim {0.35ex}

% Height of a dit or dah bar above the baseline
% Default is to set it to the math axis height, the height of a minus sign.
\box_new:N \l__mathaxis_box
\hbox_set:Nn \l__mathaxis_box {$\textstyle\vcenter{}$} %
\dim_new:N \l__mathaxis_hgt_dim 
\dim_set:Nn \l__mathaxis_hgt_dim {\box_ht:N \l__mathaxis_box}

\dim_new:N \g__cw_charhgt_dim
\dim_gset:Nn \g__cw_charhgt_dim {\l__mathaxis_hgt_dim - \g__cw_charthick_dim / 2}
% Could instead set it directly, as here
% \dim_gset:Nn \g__cw_charhgt_dim {2.5pt}

% Left to right width of a dit bar
\dim_new:N \g__cw_ditwidth_dim
\dim_gset:Nn \g__cw_ditwidth_dim {0.20em}

% If true, dah's are shown as a circle
\bool_new:N \l_cw_dit_dot_bool  
\bool_set_false:N \l_cw_dit_dot_bool

% Factor to rescale a period to get a dot
\tl_new:N \g__cw_dot_scale_factor_tl
\tl_gset:Nn \g__cw_dot_scale_factor_tl {1.5}

% How far to raise a dot
\dim_new:N \g__cw_dot_raisebox_dim
\dim_gset:Nn \g__cw_dot_raisebox_dim {1.70pt}

% Width of a dah bar
\dim_new:N \g__cw_dahwidth_dim
\dim_gset:Nn \g__cw_dahwidth_dim {3\g__cw_ditwidth_dim}

% Space between dits and dahs inside of a character
\dim_new:N \g__cw_intrachar_space_dim
\tl_new:N \g__cw_intrachar_space_factor_tl
\tl_gset:Nn \g__cw_intrachar_space_factor_tl {1}
\dim_gset:Nn \g__cw_intrachar_space_dim {\g__cw_intrachar_space_factor_tl\g__cw_ditwidth_dim}

% Space between characters
\dim_new:N \g__cw_interchar_space_dim
\tl_new:N \g__cw_interchar_space_factor_tl
\tl_gset:Nn \g__cw_interchar_space_factor_tl {3}
\dim_gset:Nn \g__cw_interchar_space_dim {\g__cw_interchar_space_factor_tl\g__cw_ditwidth_dim}

% Space between words
%  Could have chosen a skip, but the design goal is not paragraphs of morse,
% it is short snatches.  So chose a dim.
\dim_new:N \g__cw_interword_space_dim
\tl_new:N \g__cw_interword_space_factor_tl
\tl_gset:Nn \g__cw_interword_space_factor_tl {7}
\dim_gset:Nn \g__cw_interword_space_dim {\g__cw_interword_space_factor_tl\g__cw_ditwidth_dim}

% Shade of gray for \morse command
\tl_new:N \g__cw_grayshade_tl
\tl_gset:Nn \g__cw_grayshade_tl {0.50}

% ==============================
% Style file: Options

\DeclareKeys[cw]{%
  thickness.code =  {\dim_gset:Nn \g__cw_charthick_dim {#1}},  % How thick is the dit or dah, length 
  height.code    =  {\dim_gset:Nn \g__cw_charhgt_dim {#1}},     % Dist above the baseline, length 
  ditwidth.code  = {\dim_gset:Nn \g__cw_ditwidth_dim {#1}},   % Width of a dit, length
  dahwidth.code  =  {\dim_gset:Nn \g__cw_dahwidth_dim {#1}},   % Width of a dah, length
  intracharfactor.code =  {\tl_gset:Nn \g__cw_intrachar_space_factor_tl {#1}},  % Multiply this by dit width to get intrachar space
  intercharfactor.code =  {\tl_gset:Nn \g__cw_interchar_space_factor_tl {#1}},  % Multiply this by dit width to get interchar space
  interwordfactor.code =  {\tl_gset:Nn \g__cw_interword_space_factor_tl {#1}},  % Multiply this by dit width to get interword space
  grayshade.code =  {\tl_gset:Nn \g__cw_grayshade_tl {#1}},  % Gray shade for \morse command
  ditdot.code    =  {\bool_set_true:N \l_cw_dit_dot_bool},
  dotscale.code  =  {\tl_gset:Nn \g__cw_dot_scale_factor_tl {#1}},  % Magnify dit dot by this factor   
  dotraise.code  =  {\dim_gset:Nn \g__cw_dot_raisebox_dim {#1}},  % Raise dit dot after rescaling
}

\ProcessKeyOptions[cw]

% Set the spaces based on the factors
\dim_gset:Nn \g__cw_intrachar_space_dim {\g__cw_intrachar_space_factor_tl\g__cw_ditwidth_dim}
\dim_gset:Nn \g__cw_interchar_space_dim {\g__cw_interchar_space_factor_tl\g__cw_ditwidth_dim}
\dim_gset:Nn \g__cw_interword_space_dim {\g__cw_interword_space_factor_tl\g__cw_ditwidth_dim}

% Show everything in the log
\dim_log:N \g__cw_charthick_dim
\dim_log:N \g__cw_charhgt_dim
\dim_log:N \g__cw_ditwidth_dim
\dim_log:N \g__cw_dahwidth_dim
\dim_log:N \g__cw_intrachar_space_dim
\dim_log:N \g__cw_interchar_space_dim
\dim_log:N \g__cw_interword_space_dim
\tl_log:N \g__cw_grayshade_tl
\bool_log:N \l_cw_dit_dot_bool
\tl_log:N \g__cw_dot_scale_factor_tl
\dim_log:N \g__cw_dot_raisebox_dim


% ==============================
% Style file: More declarations


% ==============================
% User-facing commands

% Just a single word
\NewDocumentCommand{\cwWord}{m}
 {
   \cw_word:n {#1}
 }

% Can have spaces inside
\NewDocumentCommand{\cwMessage}{m}
 {
   \cw_message:n {#1}
 }

% Like the prior but shaded gray so ti does not to jump off the page so much
\definecolor{morsecolor}{gray}{\g__cw_grayshade_tl}
\NewDocumentCommand{\morse}{m}
 {
   {\color{morsecolor}\cw_message:n {#1}}
 }


% ==============================
% Internal commands

% ........ \cwDit and \cwDah
\newcommand\cwDitDot{%
  \makebox[\g__cw_ditwidth_dim][c]{\raisebox{\g__cw_dot_raisebox_dim}{\scalebox{\g__cw_dot_scale_factor_tl}{.}}}}

\newcommand\cwDitDash{%
  \rule[\g__cw_charhgt_dim]{\g__cw_ditwidth_dim}{\g__cw_charthick_dim}}

\newcommand\cwDah{%
  \rule[\g__cw_charhgt_dim]{\g__cw_dahwidth_dim}{\g__cw_charthick_dim}}

\bool_if:nTF \l_cw_dit_dot_bool { \newcommand\cwDit{\cwDitDot} } { \newcommand\cwDit{\cwDitDash} } 

% ............ Enlarge for fiddling 
% From https://tex.stackexchange.com/a/85621/339
% #1 = width, #2 = height
\newcommand{\rectangle}[2]{{\setlength{\fboxrule}{0.2pt}
  \fboxsep=-\fboxrule\sbox0{}\wd0=#1\ht0=#2\relax\fbox{\box0}}}

% Tool to help visually place the dot.  It enlarges the dot and puts it in a 
% 1ex tall box that is the width of a dit, and with a hairline at the height 
% of a minus sign.  Then you can play with \g__cw_dot_scale_factor_tl and
% \g__cw_dot_raisebox_dim to make it the right width and elevation.
\NewDocumentCommand\cwEnlargeDit{ O{15}}{%
  \scalebox{#1}{%
    \makebox[0pt][l]{\color{green}\rectangle{\g__cw_ditwidth_dim}{1ex}}%
    \makebox[\g__cw_ditwidth_dim][c]{\cwDit}%
    \hspace*{-\g__cw_ditwidth_dim}\raisebox{\l__mathaxis_hgt_dim}{\color{red}\rule{\g__cw_ditwidth_dim}{0.1pt}}%
  }
}

\NewDocumentCommand\cwEnlargeDah{ O{15}}{%
  \scalebox{#1}{%
    \makebox[0pt][l]{\color{green}\rectangle{\g__cw_dahwidth_dim}{1ex}}%
    \makebox[\g__cw_dahwidth_dim][c]{\cwDah}%
    \hspace*{-\g__cw_dahwidth_dim}\raisebox{\l__mathaxis_hgt_dim}{\color{red}\rule{\g__cw_dahwidth_dim}{0.1pt}}%
  }
}

% ==========================================
% Create a map from inputs to morse outputs

% First is an easy to read one, which we will then convert to one with 
% intracharacter spaces.  (When we do that we also add capital keys for those
% chars that have a capital.)

\prop_const_from_keyval:Nn \l__cw_source_letters
  {
  a = {.-},   
  b = {-...},
  c = {-.-.},
  d = {-..},
  e = {.},
  f = {..-.},
  g = {--.},
  h = {....},
  i = {..},
  j = {.---},
  k = {-.-},
  l = {.-..},
  m = {--},
  n = {-.},
  o = {---},
  p = {.--.},
  q = {--.-},	
  r = {.-.},
  s = {...},
  t = {-},
  u = {..-}, 	
  v = {...-},
  w = {.--},
  x = {-..-},
  y = {-.--},
  z = {--..},
  0 = {-----},
  1 = {.----},
  2 = {..---},
  3 = {...--},
  4 = {....-},
  5 = {.....},
  6 = {-....},
  7 = {--...},
  8 = {---..},
  9 = {----.},
  . = {.-.-.-},
  {,} = {--..--},  % braces to distinguish this from an end of line comma
  ? = {..--..},
  ' = {.----.},
  ! = {-.-.--},
  / = {-..-.},
  ( = {-.--.},
  ) = {-.--.-},
  & = {.-...},
  {:} = {---...},
  ; = {-.-.-.},
  {=} = {-...-},
  + = {.-.-.},
  - = {-....-},
  {_} = {..--.-},
  " = {.-..-.},
  {$} = {...-..-},
% $ Matches the dollar sign in the proplist so the editor gets coloring right
  % In all of history will the following ever used?
  à = {.--.-},
  ä = {.-.-},
  å = {.--.-},  % shared with another a-like
  ą = {.--.-},  % others also share
  æ = {.-.-},
  ć = {-.-..},
  ĉ = {-.-..},
  ç = {-.-..},
  % {ch} = {----},  % ??
  đ = {..-..},
  ð = {..--.},
  é = {..-..},
  è = {.-..-},
  ę = {..-..},
  ĝ = {--.-.},  % ? Esperanto
  ĥ = {----},
  ĵ = {.---.},
  ł = {.-..-},
  ń = {--.--},
  ñ = {--.--},
  ó = {---.},
  ö = {---.},
  ø = {---.},
  ś = {...-...},
  ŝ = {...-.},
  š = {----},
  þ = {.--..},
  ü = {..--},
  ŭ = {..--},
  ź = {--..-.},
  ż = {--..-}
  }
% \prop_log:N \l__cw_source_letters

% Convert . to \cwDit and - to \cwDah
\cs_set:Nn \cw_convertsyms:n {
  \if_charcode:w #1 .
    \cwDit  
  \else
    \if_charcode:w #1 -
      \cwDah
    \else
      \PackageError{cw}{Input~is~neither~a~.~nor~a~-}{Input~characters~to~convert~to~morse~symbols~must~be~either~.~for~dit~or~-~for~dah}
    \fi
  \fi
}  

% Put a intracharacter space between dits and dahs. 
\bool_new:N \l_cw_format_char_firstitem_flag

\cs_set_protected:Nn \cw_format_char:n {
  \bool_set_true:N \l_cw_format_char_firstitem_flag
  \tl_map_inline:nn {#1}
  {
    \bool_if:NTF \l_cw_format_char_firstitem_flag 
    {
      \bool_set_false:N \l_cw_format_char_firstitem_flag
      \cw_convertsyms:n
    } { 
      \hspace{\g__cw_intrachar_space_dim} \cw_convertsyms:n 
    }
    {##1}
  }
}

% ...... Make the map that users will use
% Make the prop list from letters to their morse string equivalent
% The morse string equivalent has intracharacter spaces between
% the dits and dahs.
\prop_new_linked:N \g__cw_alphabet_prop

% #1 is the key, #2 is the value
\cs_set:Nn \cw_store_in_db:nn {
  \prop_gput:Nnn \g__cw_alphabet_prop {#1} {#2}
}

\cs_generate_variant:Nn \cw_store_in_db:nn {xx}

\tl_new:N \l__cw_upper  % Key is also done in uppercase, if there is such a thing
\tl_new:N \l__cw_formatted % The value has spaces put between dits and dahs

\cs_set:Nn \cw_store_kv_in_db:nn {
  % Include the lower case key
  \prop_gput:Nnx \g__cw_alphabet_prop {#1} { \cw_format_char:n {#2} }
  % Also include the matching upper case key (if no upper case exists then
  % \str_uppercase:n returns the original key)    
  \tl_set:Nx \l__cw_upper { \str_uppercase:n {#1} } 
  \cw_store_in_db:xx {\l__cw_upper} { \cw_format_char:n {#2} }
}

\prop_map_function:NN \l__cw_source_letters \cw_store_kv_in_db:nn

% For debugging: look at the map
% \prop_log:N \g__cw_alphabet_prop

\newcommand{\cwGetLetter}[1]{%
  \prop_item:Nn  \g__cw_alphabet_prop {#1}
}


% From a word get its morse replacement
\bool_new:N \l_cw_word_firstitem_flag
\cs_set:Nn \cw_word:n 
  {
    \bool_set_true:N \l_cw_word_firstitem_flag
    \mbox{% don't want to break a morse word
      \tl_map_inline:nn {#1} {
        \bool_if:NTF \l_cw_word_firstitem_flag {
          \bool_set_false:N \l_cw_word_firstitem_flag
          \cwGetLetter 
        } {
          \hspace{\g__cw_interchar_space_dim} \cwGetLetter
        }
        {##1}
      }
    }
  }

% From a sequence of words, get the morse message
\seq_new:N \l_cw_message_seq
\bool_new:N \l_cw_message_firstitem_flag
\cs_set:Nn \cw_message:n 
  {
    \seq_set_split:Nnn \l_cw_message_seq {~} {#1} % break into words on spaces
    \seq_remove_all:Nn \l_cw_message_seq  {} % remove empty items of sequence
    \bool_set_true:N \l_cw_message_firstitem_flag
    \seq_map_inline:Nn \l_cw_message_seq {
        \bool_if:NTF \l_cw_message_firstitem_flag {
          \bool_set_false:N \l_cw_message_firstitem_flag
          \cwWord 
        } {
          \hspace{\g__cw_interword_space_dim} \cwWord
        }
        {##1}
    }
  }



 
