How to Convert CSS Style of a HTML Doc to Inline Style?

Hi,

I have HTML files exported from Scrivener. It contains CSS styles in <head>...</head> (not in a separate CSS file). I need to convert these styles to inline style. How can I accomplish it in KM?

What I would like to do is this:

  1. When a FileName.html file is added to a folder, KM reads the file into a variable.
  2. Convert the CSS styles into inline styles.
  3. Write the converted content back to that file.

Steps 1 and 3 may be done easily. But I don't know how to do Step 2.

There are websites that offers this service, such as https://premailer.io/ and mailchimp.com.

I've also found discussions from CSS style to inline style via JavaScript - Stack Overflow, and

and some scripts such as JavaScript https://gist.github.com/nirvanatikku/1034b8f4445efe831616e6f6e594615f and node.js https://github.com/lukehorvat/computed-style-to-inline-style. But I don't know how to implement them in Keyboard Maestro.

Below is a sample of an HTML file exported from Scrivener.
Preferably, I would like to remove some unnecessary styles (such as margin: 0.0px 0.0px 0.0px 0.0px and -webkit-text-stroke: #000000) to make the file look cleaner.

HTML content sample (click to unfold)
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta http-equiv="Content-Style-Type" content="text/css">
  <title></title>
  <meta name="Generator" content="Cocoa HTML Writer">
  <meta name="CocoaVersion" content="2022.6">
  <style type="text/css">
    p.p1 {margin: 0.0px 0.0px 12.0px 0.0px; font: 14.0px Times; color: #000000; -webkit-text-stroke: #000000}
    li.li3 {margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px Times; color: #000000; -webkit-text-stroke: #000000}
    span.s1 {font-kerning: none}
    span.s2 {-webkit-text-stroke: 0px #000000}
    ul.ul1 {list-style-type: disc}
    ul.ul2 {list-style-type: circle}
  </style>
</head>
<body>
<p class="p1"><span class="s1">Hello,</span></p>
<p class="p1"><span class="s1">This is a reminder that we will have a zoom meeting today at <b>7 PM Eastern Time</b>. (If you live in another time zone, please adjust accordingly).</span></p>
<h2 style="margin: 0.0px 0.0px 14.9px 0.0px; font: 14.0px Times; color: #000000; -webkit-text-stroke: #000000"><span class="s1"><b>Attendance Quiz</b></span></h2>
<ul class="ul1">
  <li class="li3"><span class="s2"></span><span class="s1">After the meeting, you must complete the attendance quiz within a week. I strongly recommend that you do the quiz as soon as each meeting ends.</span></li>
</ul>
<h2 style="margin: 0.0px 0.0px 14.9px 0.0px; font: 14.0px Times; color: #000000; -webkit-text-stroke: #000000"><span class="s1"><b>Requirement for the Zoom Meetings (LiveSyncs)</b></span></h2>
<ul class="ul1">
  <li class="li3"><span class="s2"></span><span class="s1">All Zoom meetings are mandatory. If you can’t attend, you must let you OTA know in advance.</span></li>
</ul>
<h2 style="margin: 0.0px 0.0px 14.9px 0.0px; font: 14.0px Times; color: #000000; -webkit-text-stroke: #000000"><span class="s1"><b>To join the meetings</b></span></h2>
<ul class="ul1">
  <li class="li3"><span class="s2"></span><span class="s1">Please go to the Zoom page on Canvas.</span></li>
  <li class="li3"><span class="s2"></span><span class="s1">You will need to log in to Zoom in order to join the meetings.</span>
  <ul class="ul2">
    <li class="li3"><span class="s2"></span><span class="s1">Your Zoom account does not need to be the school email.</span></li>
    <li class="li3"><span class="s2"></span><span class="s1">A couple of students used to have trouble joining the meetings with an organizational account. If you happened to encounter this issue, try a personal Zoom account.</span></li>
  </ul></li>
</ul>
<h2 style="margin: 0.0px 0.0px 14.9px 0.0px; font: 14.0px Times; color: #000000; -webkit-text-stroke: #000000"><span class="s1"><b>Zoom Recordings</b></span></h2>
<ul class="ul1">
  <li class="li3"><span class="s2"></span><span class="s1">All Zoom meetings are recorded. To access the recordings, go to the Zoom page on Canvas and click the “Cloud Recordings” tab.</span></li>
  <li class="li3"><span class="s2"></span><span class="s1">Note: Cloud recordings will be deleted automatically after they have been stored for 30 days.</span></li>
</ul>
<h2 style="margin: 0.0px 0.0px 14.9px 0.0px; font: 14.0px Times; color: #000000; -webkit-text-stroke: #000000"><span class="s1"><b>Due Date for the Meeting Reports</b></span></h2>
<ul class="ul1">
  <li class="li3"><span class="s2"></span><span class="s1">The Zoom meeting report quiz is due the 7th day after the day of meeting.<br>
</span></li>
  <li class="li3"><span class="s2"></span><span class="s1">NOTE: the due dates for the survey quizzes are the same as the meeting start time. They will not be closed until a week after each meeting. This is to remind you of the meeting start time. Since you can only start taking these quizzes after the meeting, these quizzes will always be “late submissions.” But you will not be penalized for that. These are the ONLY 6 quizzes that allow “late” submission.</span></li>
</ul>
<h2 style="margin: 0.0px 0.0px 14.9px 0.0px; font: 14.0px Times; color: #000000; -webkit-text-stroke: #000000"><span class="s1"><b>Late Work Policy</b></span></h2>
<ul class="ul1">
  <li class="li3"><span class="s2"></span><span class="s1">No late work submission is accepted for any of the assignments. I strongly recommend you submit the attendance report as soon as the meeting ends.</span></li>
</ul>
</body>
</html>

Any help appreciated!

You would search for class="ul2" (for example) and replace it with style="list-style-type: circle". For each style listed in the header.

A brutal RegEx search might be the last resort. What I'm hoping to get is to use JavaScript to do the job.

I have found a JavaScript that I can use to run in a KM HTML prompt window.

transferAll();

    function transferComputedStyle(node) {
        var cs = getComputedStyle(node, null);
        var i;
        for (i = 0; i < cs.length; i++) {
            var s = cs[i] + "";
              node.style[s] = cs[s];
        }
    }
    function transferAll() {
        var all = document.getElementsByTagName("*");
        var i;
        for (i = 0; i < all.length; i++) {
            transferComputedStyle(all[i]);
        }
    }

But it adds a lot more styles to the html file; most did not exist in the CSS head.

For a simple Hello everyone paragraph, it becomes:

The approach I suggested may be brutal but it isn't a regular expression. It's simply a literal search and replace for each HTML tag in the header that has been styled.

You can easily do that in your language of choice, of course. And I'd use an Execute ... action rather than a bunch of Keyboard Maestro S&R actions.

You could, for extra credit, parse the header's style section to build a list of tags to search and what their replacements are before feeding each of them to a function to do the work.

Then it wouldn't matter what deviations Scrivener produces on compile and it won't bloat your output file either.

Thanks, @mrpasini.

Can you show a little bit more about how to accomplish it?
In the sample code, I have these CSS style:

  <style type="text/css">
    p.p1 {margin: 0.0px 0.0px 12.0px 0.0px; font: 14.0px Times; color: #000000; -webkit-text-stroke: #000000}
    li.li3 {margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px Times; color: #000000; -webkit-text-stroke: #000000}
    span.s1 {font-kerning: none}
    span.s2 {-webkit-text-stroke: 0px #000000}
    ul.ul1 {list-style-type: disc}
    ul.ul2 {list-style-type: circle}
  </style>

I will need to get get these styles inside {style} one by one. This has to be a RegEx search, right?
Then, I'll seed to replace class="p1" with the information in {}, something like style="style". This is another RegEx search and replace.

Is this what you suggest?

Here's a quick and dirty Perl script and your HTML text in a separate file to illustrate the concept.

Archive.zip (2.6 KB)

Here's what it's doing:

  • Read the file contents into a variable
  • Read the style definitions in the header into another variable and delete them from the header
  • Create an array of style definitions
  • For each style definition, read the class tag and the style of that tag
  • Search that class tag in the file contents variable and replace it with the style definition
  • Print the revisions and write them to styled.html

Now hello.html and styled.html will look identical but be coded as you prefer.

Here's the Perl for reference:

#!/usr/bin/perl

my $html = "hello.html";
my ($t, $s);

open(IN, "< $html") || die "Could not open HTML file.\n";
undef $/;
$data = <IN>;
close(IN);

$data =~ s/<style .+?>\s*(.+?)\s*<\/style>\n//ms;
my $styles = $1;
my @tags = split /\n/, $styles;
foreach my $tag (@tags) {
	$tag =~ /.+?\.(.+?)\s/;
	$t = $1;
	$tag =~ /{(.+?)}/;
	$s = $1;
	$data =~ s/class="$t"/style="$s"/g;
}
print "Results:\n\n$data";
open(OUT, "> styled.html") || die "Could not open HTML file.\n";
print OUT $data;
close(OUT);

The usual disclaimers apply :grinning_face_with_smiling_eyes:

1 Like

Thanks a lot, @mrpasini, for taking time to make the script!!

Pardon my ignorance. I have not learned anything about Perl. I need some extra help.

I have been trying to make it to work but have so far failed.
Here is what I have tried.

  • I set KM variable name to the path of the html file and revise the $html variable in Perl to the KM variable.
  • I have discarded the variable and use the file path instead and put in quotes.
    Both gives me an error:
Could not open HTML file.

So I tried to run the test file in Terminal.

  • First, I cd.ed to the Archive directory. Then run sh styler.pl. It gives me error report:
styler.pl: line 3: my: command not found
styler.pl: line 4: syntax error near unexpected token `$t,'
styler.pl: line 4: `my ($t, $s);'

Sorry, I should have put the pieces together in Keyboard Maestro. Here's a macro that reads the contents of the original file into a local variable which the Perl script modifies before writing the revisions.

You do have to change the path from /Users/mrpasini/Desktop/ in both actions. And put hello.html on your Desktop. You'll find styled.html on your Desktop after you run the macro. The results are also displayed in a window.

Restyle.kmmacros (2.9 KB)

1 Like

Thanks again, @mrpasini!
This is great!
It's working like a charm!

Glad to hear that!

In something I'm working on (to be released in the next day or so) I take the in-line style and split at semicolons. I then filter to honour only the style pieces I actually want. (In my case it's starting with color and background-color.)

My code is Python (which gives my followers some idea which project this is for). :slight_smile:

This refinement to the solution might help you.

1 Like

That’s great! Please keep me updated!
I’ve been playing with the Perl script to remove the unwanted styles. I once thought I would never be able to understand Perl, but I start to appreciate it more. I might start using it for RegEx search/replace b/c it’s so terse. But your python script will be most welcome!

1 Like

Hey Martin,

I highly recommend it.

You won't find any language that has better or more powerful search/replace capabilities in such a terse (or not) package.

That said – the next language on my list to learn more of is Python.

-Chris

1 Like

The function in md2pptx I have is:

def handleSpanStyle(run, styleText):
    styleElements = styleText.split(";")

    # Handle the non-empty ones - as the empty one is after the final semicolon
    for styleElement in list(filter(lambda e: e != "", styleElements)):
        styleElementSplit = styleElement.split(":")
        styleElementName = styleElementSplit[0].strip()
        styleElementValue = styleElementSplit[1].strip()

        if styleElementName == "color":
            check, RGBstring = parseRGB(styleElementValue)
            if check:
                run.font.color.rgb = RGBColor.from_string(RGBstring)
            else:
                print(f"Invalid {styleElementName} RGB value {styleElementValue}")

        elif styleElementName == "background-color":
            check, RGBstring = parseRGB(styleElementValue)
            if check:
                set_highlight(run, RGBstring)
            else:
                print(f"Invalid {styleElementName} RGB value {styleElementValue}")

        elif styleElementName == "text-decoration":
            if styleElementValue == "underline":
                run.font.underline = True

        elif styleElementName == "font-weight":
            if styleElementValue == "bold":
                run.font.bold = True

        elif styleElementName == "font-style":
            if styleElementValue == "italic":
                run.font.italic = True


You can probably ignore the (text) runs bits of it. The styleText parameter is everything in the quotes after style=.

Basically it:

  1. Splits at semicolons to get colon-separated pairs.
  2. Splits at the colon to get the name and the value.

As you can see I'm only doing stuff with a tiny subset if what style= can do.

1 Like

Ah, missed that yesterday. But it's bone simple to add to the Perl script right after the foreach loop. The thing is you have to know precisely what it is you want to delete.

$data =~ s/margin: 0\.0px .+?; ?//g;
$data =~ s/<span style="-webkit-text-stroke.+?\/span>//g;

The second deletion is a bit tricky because it isn't always followed by a closing span but some other style insertion. But it makes a cleaner file.

Restyle.kmmacros (2.9 KB)

1 Like

No problem!
I was able to study your code and understood enough to make changes to suit my needs.
Instead of removing the unwanted styles from $data after the replaces, I removed them from $styles before doing the replaces.

I may need to make more changes depending on my future needs, but this is how my code looks like now:

#!/usr/bin/perl

my $file = $ENV{KMVAR_FilePath};
my ($t, $s);

open(IN, "< $file") || die "Could not open HTML file.\n";
undef $/;
$data = <IN>;
close(IN);

$data =~ s/\x{C2}\x{A0}//g;
$data =~ s/<style .+?>\s*(.+?)\s*<\/style>\n//ms;
my $styles = $1;

$styles =~ s/(margin: .*?; ?)|(color: #000000;? ?)|(color: #222d35;? ?)|( -webkit-text-stroke: #000000)|(text-decoration: underline\s?;? ?)|(font-kerning: none;? ?)|(font: \d+\.0px 'Times New Roman'; ?)|(min-height: \d+\.0px)//g;

$styles =~ s/.*?\{\s*\}\s?//g;

$data =~ s/.+<body>\n/<body style="font: 16px 'Times New Roman';">\n/ms;
$data =~ s/\s<\/html>//ms;
$data =~ s/ ?class="Apple-converted-space"//g;

my @tags = split /\n/, $styles;
foreach my $tag (@tags) {
	$tag =~ /.+?\.(.+?)\s/;
	$t = $1;
	$tag =~ /{(.+?)}/;
	$s = $1;
	$data =~ s/class="$t"/style="$s"/g;
}

$data =~ s/ class="\w+"//g;
$data =~ s/<span>\s*<\/span>//g;

#print $styles;
#print $data;

open(OUT, "> $file") || die "Could not open HTML file.\n";
print OUT $data;
close(OUT);

Thank you very much. I love it a lot!

1 Like