Browse Source

Initial commit

master
sloum 10 months ago
commit
533a972ad0
4 changed files with 435 additions and 0 deletions
  1. +61
    -0
      README.md
  2. +312
    -0
      recipe
  3. +24
    -0
      test-recipe.rcp
  4. +38
    -0
      test.html

+ 61
- 0
README.md View File

@ -0,0 +1,61 @@
# recipe
`recipe` is a text format for creating recipes and outputting those recipes in various common formats.
## File Structure
`recipe` is a tab delimited format with a leader character to tell what the line represents. Built in leaders are as follows:
```
@ Meta Information
% Ingredient
> Instruction
* Note
```
### Meta Information
Meta info lines are three columns: the leader, the key, and the value. `recipe` will look for the meta field 'Title' and use it as a heading, where available.
You can include any meta info you want as well. Here is an example of meta information:
```
@ Title The Best Vegan Pancakes
@ Author Jane Doe
@ Date 2019-10-10
```
All meta fields are optional.
### Ingredient
Ingredients come in four columns: leader, amount, measure, and item. Measure is an optional field, but remember to add a tab to represent it. Any ingredient lines without four columns will be ommitted from output.
Example:
```
% 2 T Oil (any neutral oil is fine)
% 1 C Flour
% 2 tsp Sugar
% 1 Banana, sliced
```
I find that using one character for the `measure` column works nicely, but `recipe` will align things either way so use what feels comfortable to you. If you like `cups` rather than `C`, go for it.
### Instruction and note
Instruction and note lines are the easiest. They are two column: the leader and the text content. In the case of instructions `recipe` will keep track of what instruction number has been entered and automatically number the instructions when outputting the recipe. Notes will appear under their own header and will be un-numbered list items at the end of the recipe.
```
> Pour water into dry mixture and whisk until there are no more lumps.
* I find that this recipe tastes better after settling in the fridge for a few hours.
```
### Other
Blank lines in the file will be ignored, as will any lines that dont conform to one of the required formats.
## Output
`recipe` supports output as HTML, Markdown, text/gemini, and plaintext

+ 312
- 0
recipe View File

@ -0,0 +1,312 @@
#! /usr/bin/env python3
import sys, os.path, textwrap
# Constants
META = '@'
INGREDIENT = '%'
INSTRUCTION = '>'
NOTE = '*'
CSS_BODY_BG = '#C7FCC8'
CSS_MAIN_BG = '#F9D5AF'
CSS_MAIN_ACCENT = '#F6F695'
CSS_FONT_COLOR = '#111'
CSS_FONT_SIZE = '15px'
CSS_FONT_FAMILY = 'monospace'
CSS_MAIN_MAX_WIDTH = '70ch'
CSS = '''<style>
body {{ padding: 2rem; font-size: {}; font-family: {}; background-color: {};}}
main {{width: 90%; max-width: {}; border: 1.5ch solid {}; background-color: {}; color: {}; margin: auto; padding: 4ch;}}
dd, li {{margin-bottom: 1rem;}}
dt {{font-weight: bold;}}
dl, h1 {{text-align: center;}}
dd {{margin-left: 0;}}
</style>
'''
# Global vars
recipe = {'meta': {}, 'ingredients': [], 'instructions': [], 'notes': []}
print_to_stdout = False
def add_meta(l):
data = l.split('\t')
if len(data) != 3:
return
key = data[1].strip().lower()
value = data[2].strip()
if key and value:
recipe['meta'][key] = value
def add_ingredient(l):
data = l.split('\t')
if len(data) != 4:
return
amount = data[1].strip()
measure = data[2].strip()
item = data[3].strip()
if amount and item:
recipe['ingredients'].append({'amount': amount, 'measure': measure, 'item': item})
def add_text(l, kind):
data = l.split('\t')
if len(data) != 2:
return
data[1] = data[1].strip()
if not data[1]:
return
if data[0] == INSTRUCTION:
recipe['instructions'].append(data[1])
elif data[0] == NOTE:
recipe['notes'].append(data[1])
def process_file(path):
if os.path.exists(path):
with open(path, 'r') as f:
while True:
line = f.readline()
if not line:
break
if len(line) < 1:
continue
if line[0] == META:
add_meta(line)
elif line[0] == INGREDIENT:
add_ingredient(line)
elif line[0] == INSTRUCTION:
add_text(line, 'instructions')
elif line[0] == NOTE:
add_text(line, 'notes')
if not len(recipe['ingredients']) and not len(recipe['instructions']):
print("There was not enough recipe information found in the file", file=sys.stderr)
sys.exit(1)
else:
print("The requested path does not exist", file=sys.stderr)
sys.exit(1)
def output_html(path):
meta_count_needed = 0
out = [
'<html lang="en"><head>',
CSS.format(
CSS_FONT_SIZE,
CSS_FONT_FAMILY,
CSS_BODY_BG,
CSS_MAIN_MAX_WIDTH,
CSS_MAIN_ACCENT,
CSS_MAIN_BG,
CSS_FONT_COLOR),
'</head><body><main>']
if 'title' in recipe['meta']:
out.insert(1, '<title>Recipe: {}</title>'.format(recipe['meta']['title']))
out.append('<h1>{}</h1>'.format(recipe['meta']['title']))
meta_count_needed = 1
if len(recipe['meta']) > meta_count_needed:
out.append('<dl>')
for key in recipe['meta']:
if key == 'title':
continue
out.append('<dt>{}</dt>'.format(key.title()))
out.append('<dd>{}</dd>'.format(recipe['meta'][key]))
if len(recipe['meta']) > meta_count_needed:
out.append('</dl><hr>')
if len(recipe['ingredients']):
out.append('<h2>Ingredients</h2>')
out.append('<table id="ingredients" role="presentation">')
for ingredient in recipe['ingredients']:
out.append('<tr><td>{}</td><td>{}</td><td>{}</td></tr>'.format(ingredient['amount'], ingredient['measure'], ingredient['item']))
out.append('</table>')
if len(recipe['instructions']):
out.append('<h2>Instructions</h2>')
out.append('<ol>')
for ins in recipe['instructions']:
out.append('<li>{}</li>'.format(ins))
out.append('</ol>')
if len(recipe['notes']):
out.append('<h2>Notes</h2>')
out.append('<ul>')
for note in recipe['notes']:
out.append('<li>{}</li>'.format(note))
out.append('</ul>')
out.append('</main></body></html>')
if print_to_stdout:
print_recipe(out)
else:
write_file(out,'.html', path)
def output_markdown(path):
meta_count_needed = 0
out = []
if 'title' in recipe['meta']:
out.append('# {}'.format(recipe['meta']['title']))
meta_count_needed = 1
if len(recipe['meta']) > meta_count_needed:
out.append('')
for key in recipe['meta']:
if key == 'title':
continue
out.append('**{}**: {}'.format(key.title(), recipe['meta'][key]))
out.append('---')
if len(recipe['ingredients']):
out.append('## Ingredients\n')
for ingredient in recipe['ingredients']:
out.append('- _{} {}_ {}'.format(ingredient['amount'], ingredient['measure'], ingredient['item']))
out.append('')
if len(recipe['instructions']):
out.append('## Instructions\n')
for ins in recipe['instructions']:
out.append('1. {}'.format(ins))
out.append('')
if len(recipe['notes']):
out.append('## Notes\n')
for note in recipe['notes']:
out.append('- {}'.format(note))
out.append('')
if print_to_stdout:
print_recipe(out)
else:
write_file(out, '.md', path)
def output_plaintext(path):
out = []
if 'title' in recipe['meta']:
out.append(recipe['meta']['title'].upper())
meta_count_needed = 1
if len(recipe['meta']) > meta_count_needed:
out.append('')
for key in recipe['meta']:
if key == 'title':
continue
out.append('{}: {}'.format(key.title(), recipe['meta'][key]))
out.append('')
if len(recipe['ingredients']):
out.append('== INGREDIENTS ==\n')
for ingredient in recipe['ingredients']:
out.append('{:>4} {:10}{}'.format(ingredient['amount'], ingredient['measure'], ingredient['item']))
out.append('')
if len(recipe['instructions']):
out.append('== Instructions ==\n')
for ind, ins in enumerate(recipe['instructions']):
wrapped = textwrap.wrap(ins)
out.append('Step {}:'.format(ind+1))
out.extend(wrapped)
out.append('')
out.append('')
if len(recipe['notes']):
out.append('== Notes ==\n')
for note in recipe['notes']:
wrapped = textwrap.wrap(note)
out.extend(wrapped)
out.append('')
out.append('')
out.append('')
if print_to_stdout:
print_recipe(out)
else:
write_file(out, '.txt', path)
def output_gemini(path):
meta_count_needed = 0
out = []
if 'title' in recipe['meta']:
out.append('# {}'.format(recipe['meta']['title']))
meta_count_needed = 1
if len(recipe['meta']) > meta_count_needed:
out.append('')
for key in recipe['meta']:
if key == 'title':
continue
out.append('{}: {}'.format(key.upper(), recipe['meta'][key]))
out.append('')
if len(recipe['ingredients']):
out.append('## Ingredients\n')
for ingredient in recipe['ingredients']:
out.append('{}\t{}\t{}'.format(ingredient['amount'], ingredient['measure'], ingredient['item']))
out.append('')
if len(recipe['instructions']):
out.append('## Instructions\n')
for ind, ins in enumerate(recipe['instructions']):
out.append('{:>2}. {}'.format(ind+1, ins))
out.append('')
if len(recipe['notes']):
out.append('## Notes\n')
for note in recipe['notes']:
out.append('* {}'.format(note))
out.append('')
if print_to_stdout:
print_recipe(out)
else:
write_file(out, '.gmi', path)
def build_path(recipe_path, new_suffix):
if recipe['meta']['title']:
fn = recipe['meta']['title'].replace(" ", "_").replace("\t", "_").lower() + new_suffix
root, trash = os.path.split(recipe_path)
return os.path.join(root, fn)
else:
filename, ext = os.path.splitext(recipe_path)
return recipe_path + new_suffix
def write_file(data_list, suffix, path):
new_path = build_path(path, suffix)
try:
with open(new_path, 'w') as f:
f.writelines(data_list)
print("File written: {}".format(new_path), file=sys.stderr)
except IOError:
print("Could not create file {} for writing".format(new_path), file=sys.stderr)
def print_recipe(dataList):
for line in dataList:
print(line)
def print_help():
help_text = '''recipe v0.5
Usage:
recipe [flags] [rcp-file]
Flags:
-H, --HTML Output html
-m, --markdown Output markdown
-t, --text Output plain text
-G, --gemini Output text/gemini
-s, --stdout Print the output to stdout instead of a file
-h, --help Print help and exit'''
print(help_text, file=sys.stderr)
sys.exit(1)
def parse_args():
args = sys.argv[1:]
if not len(args) or '-h' in args or '--help' in args:
print_help()
path = args.pop()
process_file(path)
if '-s' in args or '--stdout' in args:
global print_to_stdout
print_to_stdout = True
for arg in args:
if arg in ['-H', '--HTML', '--html']:
output_html(path,)
elif arg in ['-m', '--MD', '--md', '--markdown']:
output_markdown(path)
elif arg in ['-t', '--text', '--txt', '--plain']:
output_plaintext(path)
elif arg in ['-G', '--gemini', '--gmi']:
output_gemini(path)
elif arg in ['-s', '--stdout']:
continue
else:
print("Unknown flag: {}".format(arg), file=sys.stderr)
if __name__ == '__main__':
parse_args()

+ 24
- 0
test-recipe.rcp View File

@ -0,0 +1,24 @@
@ Title Instant Pot Vegan Chili
@ Servings 8
% 1 Onion, diced
% 3 Carrots, peeled and diced
% 1 Red Bell Pepper, diced
% 3 Garlic cloves, minced
% 28 oz Canned diced tomato
% 1 C Red lentils, rinsed
% 2 C Water
% 15 oz Black beans (canned), drained and rinsed
% 15 oz Kidney beans (canned), drained and rinsed
% 1 T Chili powder
% 2 t Ground cumin
% 0.5 t Smoked paprika
% 1 pinch Cayenne pepper
% 1 t Salt
> Rinse the black beans, kidney beans, and lentils until the water runs relatively clear and add to instant pot
> Add all other ingredients to instant pot
> Close lid and set steam release valve to "sealing"
> Press the "manual" or "pressure cook" button, and set to pressure cook for 10 minutes
> Let release naturally for 10 minutes and then carefully release the remaining pressure by moving the pressure release valve to "venting"
> When the float valve in the lid drops it is safe to open the lid. Do so and stir the chili well to mix flavors and further blend in the lentils
> Serve hot with choice of toppings
* This is just some test data to try things out

+ 38
- 0
test.html View File

@ -0,0 +1,38 @@
<html lang="en"><head>
<title>Recipe: Instant Pot Vegan Chili
</head><body><main>
<h1>Instant Pot Vegan Chili</h1>
<dl>
<dt>Source</dt>
<dd>https://detoxinista.com/wprm_print/29508</dd>
<dt>Servings</dt>
<dd>8</dd>
</dl><hr>
<h2>Ingredients</h2>
<table id="ingredients" role="presentation">
<tr><td>1</td><td></td><td>Onion, diced</td></tr>
<tr><td>3</td><td></td><td>Carrots, peeled and diced</td></tr>
<tr><td>1</td><td></td><td>Red Bell Pepper, diced</td></tr>
<tr><td>3</td><td></td><td>Garlic cloves, minced</td></tr>
<tr><td>28</td><td>oz</td><td>Canned diced tomato</td></tr>
<tr><td>1</td><td>C</td><td>Red lentils, rinsed</td></tr>
<tr><td>2</td><td>C</td><td>Water</td></tr>
<tr><td>15</td><td>oz</td><td>Black beans (canned), drained and rinsed</td></tr>
<tr><td>15</td><td>oz</td><td>Kidney beans (canned), drained and rinsed</td></tr>
<tr><td>1</td><td>T</td><td>Chili powder</td></tr>
<tr><td>2</td><td>t</td><td>Ground cumin</td></tr>
<tr><td>0.5</td><td>t</td><td>Smoked paprika</td></tr>
<tr><td>1</td><td>pinch</td><td>Cayenne pepper</td></tr>
<tr><td>1</td><td>t</td><td>Salt</td></tr>
</table>
<h2>Instructions</h2>
<ol>
<li>Rinse the black beans, kidney beans, and lentils until the water runs relatively clear and add to instant pot</li>
<li>Add all other ingredients to instant pot</li>
<li>Close lid and set steam release valve to "sealing"</li>
<li>Press the "manual" or "pressure cook" button, and set to pressure cook for 10 minutes</li>
<li>Let release naturally for 10 minutes and then carefully release the remaining pressure by moving the pressure release valve to "venting"</li>
<li>When the float valve in the lid drops it is safe to open the lid. Do so and stir the chili well to mix flavors and further blend in the lentils</li>
<li>Serve hot with choice of toppings</li>
</ol>
</main></body></html>

Loading…
Cancel
Save