diff --git a/src/app.py b/src/app.py index a3d67ad..0384592 100644 --- a/src/app.py +++ b/src/app.py @@ -33,7 +33,7 @@ import spelling import themes import youtube -VERSION = "1.3.0" +VERSION = "1.3.1" API = None # ProPresenter's network API on this Mac - set in run() from its preferences SUPPORT = os.path.expanduser("~/Library/Application Support/MessageNotes") LOG = os.path.expanduser("~/Library/Logs/message-notes.log") diff --git a/src/deck.py b/src/deck.py index cb5a464..7b8ed31 100644 --- a/src/deck.py +++ b/src/deck.py @@ -264,21 +264,23 @@ def scaled(state, scale): return s -def with_emphasis(state, bold, italic): +def with_emphasis(state, bold, italic, underline=False): s = dict(state) if bold: s["b"] = None if italic: s["i"] = None + if underline: + s["ul"] = None return s def make_rtf(style, paras, scale=1.0): - """paras: [(template paragraph index, [(text, bold, italic), ...]), ...]""" + """paras: [(template paragraph index, [(text, bold, italic, underline), ...]), ...]""" out = [] for k, (pi, segs) in enumerate(paras): codes, first, end, _ = style.para(pi) - runs = [(t, scaled(with_emphasis(first, b, i), scale)) for t, b, i in segs if t] + runs = [(t, scaled(with_emphasis(first, b, i, u), scale)) for t, b, i, u in segs if t] if not runs: runs = [("", scaled(first, scale))] out.append((codes, runs)) @@ -316,14 +318,14 @@ def slide_texts(tpl, s): if s.kind == "sentence": st = tpl.style("sentence", "sermoninasentence") label = st.paras[0][3].strip() if st and len(st.paras) > 1 else "" - out = [(0, [(label, False, False)])] if label else [] + out = [(0, [(label, False, False, False)])] if label else [] return {"sermoninasentence": out + [(-1, ln.segs_plain()) for ln in L]} if s.kind == "scripture": # the template's sample reference ends with a space ("Exodus 8:1 ESV "); keep that habit ref_st = tpl.style("scripture", "reference") tail = " " if ref_st and ref_st.paras[0][3].endswith(" ") and not s.ref.endswith(" ") else "" - return {"reference": [(0, [(s.ref + tail, False, False)])], - "scripture": [(0, ln.segs) for ln in L] or [(0, [("", False, False)])]} + return {"reference": [(0, [(s.ref + tail, False, False, False)])], + "scripture": [(0, ln.segs) for ln in L] or [(0, [("", False, False, False)])]} return {} @@ -370,29 +372,29 @@ def _as_point(tpl, s): def effective(st, paras): - """Paragraph runs as they really look on the slide: [[(text, bold, italic)]].""" + """Paragraph runs as they really look on the slide: [[(text, bold, italic, underline)]].""" out = [] for pi, segs in paras: _, first, _, _ = st.para(pi) base_b = "b" in first base_i = "i" in first - out.append([(t, base_b or b, base_i or i) for t, b, i in segs if t]) + out.append([(t, base_b or b, base_i or i, u) for t, b, i, u in segs if t]) return out def output_rtf(tpl, kind, name, st_out, paras): """The same text as a themed screen draws it: the theme's font and size, with only the - 'special' words (styled differently from the rest of the box) keeping bold/italic.""" + 'special' words (styled differently from the rest of the box) keeping bold/italic; underlines stay.""" st_slide = tpl.style(kind, name) - eff = effective(st_slide, paras) if st_slide is not None else [[(t, b, i) for t, b, i in segs] for _, segs in paras] + eff = effective(st_slide, paras) if st_slide is not None else [list(segs) for _, segs in paras] count = {} for para in eff: - for t, b, i in para: + for t, b, i, u in para: count[(b, i)] = count.get((b, i), 0) + len(t) base = max(count, key=count.get) if count else (False, False) out = [] for para in eff: - segs = [(t, b, i) if (b, i) != base else (t, False, False) for t, b, i in para] + segs = [(t, b, i, u) if (b, i) != base else (t, False, False, u) for t, b, i, u in para] out.append((0, segs)) return make_rtf(st_out, out) @@ -438,7 +440,7 @@ def measure_all(tpl, plan, outputs, measure): def line_height(st, measure_cache, measure): key = id(st) if key not in measure_cache: - r = measure([{"rtf": make_rtf(st, [(0, [("Xg", False, False)])]), "w": st.w}])[0] + r = measure([{"rtf": make_rtf(st, [(0, [("Xg", False, False, False)])]), "w": st.w}])[0] measure_cache[key] = max(1.0, r["h"]) return measure_cache[key] @@ -516,12 +518,12 @@ _CLAUSE = re.compile(r"(?<=[,\u2014])\s*") def split_scripture(tpl, s, measure, outputs=None): """One long scripture -> as few slides as possible that fit everywhere, cut at verse numbers if possible (then sentences, then clauses), pieces as even as possible.""" - flat = [] # (text, bold, italic) with "\n" between paragraphs + flat = [] # (text, bold, italic, underline) with "\n" between paragraphs for k, ln in enumerate(s.lines): if k: - flat.append(("\n", False, False)) + flat.append(("\n", False, False, False)) flat.extend(ln.segs) - text = "".join(t for t, _, _ in flat) + text = "".join(seg[0] for seg in flat) for level in (_VERSE_START, _SENT_END, _CLAUSE): bounds = {0, len(text)} | {i + 1 for i, ch in enumerate(text) if ch == "\n"} bounds |= {m.start() if level is _VERSE_START else m.end() for m in level.finditer(text)} @@ -572,12 +574,12 @@ def split_scripture(tpl, s, measure, outputs=None): def slice_segs(flat, a, b): out, pos = [], 0 - for t, bo, it in flat: + for t, bo, it, ul in flat: x, y = pos, pos + len(t) pos = y lo, hi = max(a, x), min(b, y) if lo < hi: - out.append((t[lo - x:hi - x], bo, it)) + out.append((t[lo - x:hi - x], bo, it, ul)) while out and not out[0][0].strip(): out.pop(0) while out and not out[-1][0].strip(): @@ -590,14 +592,14 @@ def slice_segs(flat, a, b): def split_lines(segs): lines, cur = [], [] - for t, b, i in segs: + for t, b, i, u in segs: parts = t.split("\n") for k, part in enumerate(parts): if k: lines.append(cur) cur = [] if part: - cur.append((part, b, i)) + cur.append((part, b, i, u)) lines.append(cur) return [l for l in lines if l] @@ -860,7 +862,7 @@ def speaker_rtfs(tpl, role, preacher): for nm, text in (("role", role), ("preacher", preacher)): st = tpl.style("speaker", nm) if st is not None and text: - out.append((nm, st, make_rtf(st, [(0, [(text, False, False)])]))) + out.append((nm, st, make_rtf(st, [(0, [(text, False, False, False)])]))) return out diff --git a/src/notes.py b/src/notes.py index d0c76b3..acfd6fa 100644 --- a/src/notes.py +++ b/src/notes.py @@ -76,11 +76,11 @@ QUOTE_OPEN = "\u201c\"\u2018'" class Line: - """One paragraph of slide text: segments of (text, bold, italic); lead = lead-in style.""" + """One paragraph of slide text: segments of (text, bold, italic, underline); lead = lead-in style.""" __slots__ = ("segs", "lead") def __init__(self, segs, lead=False): - self.segs = [s for s in segs if s[0]] + self.segs = [tuple(s) + (False,) * (4 - len(s)) for s in segs if s[0]] self.lead = lead @property @@ -88,7 +88,14 @@ class Line: return "".join(s[0] for s in self.segs) def segs_plain(self): - return [(self.text, False, False)] + """The text in the box's own style (bold/italic come from the theme); underlines stay.""" + out = [] + for t, _, _, u in self.segs: + if out and out[-1][3] == u: + out[-1] = (out[-1][0] + t, False, False, u) + else: + out.append((t, False, False, u)) + return out or [(self.text, False, False, False)] def __repr__(self): return "Line(%r%s)" % (self.text, ", lead" if self.lead else "") @@ -181,9 +188,14 @@ class P: col = doc.colors[ci] if ci < len(doc.colors) else None if col in ((0, 0, 0), (255, 255, 255)): col = None - self.runs.append((r.text, bold, ital, (r.state.get("fs", 24) or 24) / 2.0, col)) + ul = "ul" in r.state + self.runs.append((r.text, bold, ital, (r.state.get("fs", 24) or 24) / 2.0, col, ul)) self.raw = "".join(r[0] for r in self.runs) self.text = clean(self.raw) + # which characters of self.text are underlined in the notes + mask = [u for r in self.runs for u in [r[5]] * len(r[0])] + t, m = clean_mask(self.raw, mask) + self.ul = m if (t == self.text and any(m)) else None vis = [r for r in self.runs if r[0].strip()] self.size = max([r[3] for r in vis] or [12]) self.bold = bool(vis) and all(r[1] for r in vis) @@ -204,10 +216,51 @@ def clean(text): return t.strip() +def clean_mask(raw, mask): + """clean(), carrying a per-character flag along: (text, flags).""" + chars = [] + for ch, f in zip(raw, mask): + ch = "\n" if ch in "\u2028\u2029" else " " if ch == "\u00a0" else ch + chars.extend([(".", f)] * 3 if ch == "\u2026" else [(ch, f)]) + out = [] + for ch, f in chars: + if ch == "\n": + while out and out[-1][0] in " \t": + out.pop() + out.append((ch, f)) + while out and out[0][0].isspace(): + out.pop(0) + while out and out[-1][0].isspace(): + out.pop() + return "".join(c for c, _ in out), [f for _, f in out] + + +def underlined(text, p, part=None, offset=0): + """Segments for text (taken from paragraph p's text, or from part of it starting at offset), + underlined where the notes underline it.""" + if p is None or not p.ul: + return [(text, False, False, False)] + src = p.text if part is None else part + flags = p.ul[offset:offset + len(src)] + k, skip = src.find(text), 0 + if k < 0: # "1) Faith" became "1. Faith": match the end of it + while skip < len(text) and src.find(text[skip:]) < 0: + skip += 1 + k = src.find(text[skip:]) if skip < len(text) else 0 + mask = [False] * skip + flags[k:k + len(text) - skip] + out = [] + for ch, u in zip(text, mask + [False] * (len(text) - len(mask))): + if out and out[-1][3] == u: + out[-1][0] += ch + else: + out.append([ch, False, False, u]) + return [tuple(s) for s in out] + + def segments(runs, start_char=0, emphasis=True): - """Runs -> [(text, bold, italic)] starting at character offset start_char.""" + """Runs -> [(text, bold, italic, underline)] starting at character offset start_char.""" out, pos = [], 0 - for text, bold, ital, size, col in runs: + for text, bold, ital, size, col, ul in runs: a, b = pos, pos + len(text) pos = b if b <= start_char: @@ -215,7 +268,7 @@ def segments(runs, start_char=0, emphasis=True): piece = text[max(0, start_char - a):] if not emphasis: bold = ital = False - out.append([piece, bold, ital]) + out.append([piece, bold, ital, ul]) # verse numbers are never emphasis for s in out: if s[1] and re.fullmatch(r"\s*\d{1,3}\s*", s[0]): @@ -227,7 +280,7 @@ def segments(runs, start_char=0, emphasis=True): merged = [] for s in out: s[0] = s[0].replace("\u2026", "...").replace("\u2028", " ").replace("\u00a0", " ") - if merged and merged[-1][1] == s[1] and merged[-1][2] == s[2]: + if merged and merged[-1][1:] == s[1:]: merged[-1][0] += s[0] else: merged.append(s) @@ -369,7 +422,7 @@ def parse(data): rest = p.text[m.end():].strip() lines = [] if rest: - lines.append(Line([(rest, False, False)])) + lines.append(Line(underlined(rest, p))) i += 1 if not lines: # following paragraphs up to the next blank line @@ -377,7 +430,8 @@ def parse(data): i += 1 while i < n and not body[i].blank and not parse_ref(body[i].text): if not is_annotation(body[i]): - lines.append(Line([(body[i].text.replace("\n", " "), False, False)])) + q = body[i] + lines.append(Line(underlined(q.text.replace("\n", " "), q, q.text.replace("\n", " ")))) i += 1 s = Slide("sentence", lines, src=p.idx) if not lines: @@ -479,11 +533,13 @@ def statements_to_slides(group, force_build=False): """Consecutive statement paragraphs -> Point / Build slides.""" lines = [] for p in group: + pos = 0 for part in p.text.split("\n"): + start, pos = pos, pos + len(part) + 1 if not part.strip(): continue text, mark = strip_list_mark(part) - lines.append((text, p, mark)) + lines.append((text, p, mark, part, start)) if not lines: return [] src = group[0].idx @@ -506,8 +562,8 @@ def statements_to_slides(group, force_build=False): else: kind = "build" out_lines = [] - for k, (text, p, mark) in enumerate(lines): - out_lines.append(Line([(text, False, False)], lead=(k == 0 and lead and kind == "point"))) + for k, (text, p, mark, part, start) in enumerate(lines): + out_lines.append(Line(underlined(text, p, part, start), lead=(k == 0 and lead and kind == "point"))) s = Slide(kind, out_lines, src=src) plain = [p for p in group if not p.bold and not p.italic] if plain and all(p.size <= 15 for p in plain): @@ -533,5 +589,5 @@ def quote_slide(lines, src): attribution = [l for l in lines[1:] if re.match(r"^\s*[-\u2013\u2014~]\s*\S", l[0])] if not (first[1].italic or attribution): return None - out = [Line([(l[0], False, False)]) for l in lines] + out = [Line(underlined(l[0], l[1], l[3], l[4])) for l in lines] return Slide("quote", out, src=src) diff --git a/src/plantext.py b/src/plantext.py index d493bbd..36b78c8 100644 --- a/src/plantext.py +++ b/src/plantext.py @@ -12,6 +12,7 @@ HEADER = """# Slide list for: {name} # The lines under it are the slide's lines (one line = one paragraph; in a Build, one step). # In a Point, start a line with > to make it the italic lead-in line. # In a Scripture, put **double asterisks** around bold words and *single ones* around italic words. +# On any slide, __double underscores__ around words underline them. # Delete a slide by deleting its whole block. Blank lines and lines starting with "# " are ignored. # Lines starting with #! are notes that were left off the slides (shown for reference). """ @@ -44,38 +45,48 @@ def dump(plan, name=""): if s.kind == "scripture": out.append("[Scripture] %s" % s.ref) for ln in s.lines: - out.append("".join(mark(t, b, i) for t, b, i in ln.segs)) + out.append("".join(mark(t, b, i, u) for t, b, i, u in ln.segs)) else: out.append("[%s]" % NAMES.get(s.kind, "Point")) for ln in s.lines: - out.append(("> " if (ln.lead and s.kind == "point") else "") + ln.text) + text = "".join(mark(t, False, False, u) for t, _, _, u in ln.segs) + out.append(("> " if (ln.lead and s.kind == "point") else "") + text) for item in left.get(len(plan.slides), []): out.append("#! %s" % item[1].replace("\n", " ")) return "\n".join(out) + "\n" -def mark(t, b, i): +def mark(t, b, i, u=False): m = "***" if (b and i) else "**" if b else "*" if i else "" - if not m or not t.strip(): + if not (m or u) or not t.strip(): return t lead, core, trail = re.match(r"^(\s*)(.*?)(\s*)$", t, re.S).groups() - return "%s%s%s%s%s" % (lead, m, core, m, trail) + core = "%s%s%s" % (m, core, m) + return "%s%s%s" % (lead, "__%s__" % core if u else core, trail) -def parse_bold(text): - segs, pos = [], 0 - for m in re.finditer(r"\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|\*(.+?)\*", text): - if m.start() > pos: - segs.append((text[pos:m.start()], False, False)) - if m.group(1): - segs.append((m.group(1), True, True)) - elif m.group(2): - segs.append((m.group(2), True, False)) - else: - segs.append((m.group(3), False, True)) - pos = m.end() - if pos < len(text): - segs.append((text[pos:], False, False)) +def parse_bold(text, styles=True): + """'a **b** __c__' -> [(text, bold, italic, underline)]; styles=False keeps only underline.""" + segs = [] + # "__" + text that doesn't start or end with _ or a space + "__" (so "______" blanks stay) + for k, piece in enumerate(re.split(r"__([^_\s](?:.*?[^_\s])??)__", text)): + u = k % 2 == 1 + if not styles: + segs.append((piece.replace("**", ""), False, False, u)) + continue + pos = 0 + for m in re.finditer(r"\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|\*(.+?)\*", piece): + if m.start() > pos: + segs.append((piece[pos:m.start()], False, False, u)) + if m.group(1): + segs.append((m.group(1), True, True, u)) + elif m.group(2): + segs.append((m.group(2), True, False, u)) + else: + segs.append((m.group(3), False, True, u)) + pos = m.end() + if pos < len(piece): + segs.append((piece[pos:], False, False, u)) return [s for s in segs if s[0]] @@ -105,7 +116,9 @@ def load(text): "e.g. [Scripture] John 3:16 ESV" % no) cur.ref = ref elif m.group(2).strip(): - cur.lines.append(notesmod.Line([(m.group(2).strip(), False, False)])) + t = m.group(2).strip() + cur.lines.append(notesmod.Line([(t, False, False)] if kind in ("picture", "clearpicture") + else parse_bold(t, styles=False))) continue if cur is None: raise ParseError("line %d: text before the first [Slide type] line: %r" % (no, line.strip()[:60])) @@ -113,9 +126,9 @@ def load(text): if cur.kind == "scripture": cur.lines.append(notesmod.Line(parse_bold(t))) elif cur.kind == "point" and t.startswith(">"): - cur.lines.append(notesmod.Line([(t[1:].strip(), False, False)], lead=True)) + cur.lines.append(notesmod.Line(parse_bold(t[1:].strip(), styles=False), lead=True)) else: - cur.lines.append(notesmod.Line([(t.replace("**", ""), False, False)])) + cur.lines.append(notesmod.Line(parse_bold(t, styles=False))) if cur is not None: plan.slides.append(cur) titles = [s for s in plan.slides if s.kind == "title"] diff --git a/src/spelling.py b/src/spelling.py index 9d247ef..d3ab87d 100644 --- a/src/spelling.py +++ b/src/spelling.py @@ -2,7 +2,8 @@ A word is fixed only when macOS has a confident correction for it (the same one it would auto-correct while typing). Names it doesn't know (Rahab, Chniouli) have none, so they are -left alone. ALL-CAPS emphasis words are checked too (macOS normally skips those), and +left alone, and so are capitalised words inside scripture and names the notes' scripture +uses (Gilead). ALL-CAPS emphasis words are checked too (macOS normally skips those), and doubled small words ("in in") are fixed. Every fix is reported. To undo one, the word goes into allowed-words.txt (never touched again) and the check is simply run again on the text from before the fixes. @@ -150,6 +151,24 @@ def _starts_sentence(text, loc): return not before or before[-1] in ".!?:\u201c\"(" or before.endswith("\u2014") +_WORD = re.compile(r"[A-Za-z\u00c0-\u024f]+(?:['\u2019][A-Za-z]+)?") + + +def names_in(plan): + """Words the notes themselves use as names (lowercase): capitalised inside a scripture + passage (Gilead, Jephthah), or capitalised in the middle of a sentence anywhere.""" + out = set() + for s, _, _, text in _segments(plan): + for m in _WORD.finditer(text): + w = m.group(0) + if not w[:1].isupper() or w.isupper(): + continue + if s.kind == "scripture" or not _starts_sentence(text, m.start()): + out.add(w.lower()) + out.add(re.split(r"['\u2019]", w)[0].lower()) # Gilead's -> gilead + return out + + def check_and_fix(plan, skip=None): """Fix confident typos in the plan's slide text. Returns a Report. skip: words (lowercase) never to change, on top of allowed-words.txt.""" @@ -159,6 +178,7 @@ def check_and_fix(plan, skip=None): return rep results = _run_jxa([t for _, _, _, t in segs]) ok = allowed_words() | set(skip or ()) | SERMON_WORDS + names = names_in(plan) for (s, li, si, text), found in zip(segs, results): edits = [] # (start, end, replacement, word) for f in found: @@ -166,6 +186,10 @@ def check_and_fix(plan, skip=None): word = text[loc:loc + ln] if not word or word.lower() in ok or any(ch.isdigit() for ch in word): continue + # Bible text is pasted, and its capitalised words are names and places ("Gilead was + # the father of Jephthah" starts a sentence); a name the notes use is never a typo + if word[:1].isupper() and (s.kind == "scripture" or word.lower() in names): + continue corr = f["corr"] name_like = word[:1].isupper() and not word.isupper() and not _starts_sentence(text, loc) # "lampstand" -> "lamp stand", "firstfruits" -> "first fruits": Bible words, not typos diff --git a/tests/notes/Slides - Together Tuesday - October 2026.rtf b/tests/notes/Slides - Together Tuesday - October 2026.rtf new file mode 100644 index 0000000..14fb84d --- /dev/null +++ b/tests/notes/Slides - Together Tuesday - October 2026.rtf @@ -0,0 +1,204 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2870 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica-Bold;\f1\fswiss\fcharset0 Helvetica-BoldOblique;\f2\fswiss\fcharset0 Helvetica; +\f3\fnil\fcharset0 Verdana-Bold;\f4\fnil\fcharset0 Verdana;} +{\colortbl;\red255\green255\blue255;\red0\green0\blue0;\red0\green0\blue241;\red0\green23\blue255; +\red0\green0\blue255;} +{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;\cspthree\c0\c4769\c91973;\cssrgb\c1229\c23499\c100000; +\cssrgb\c1680\c19835\c100000;} +\vieww9260\viewh20100\viewkind0 +\deftab720 +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs54 \cf2 Together Tuesday - October 2026\ +\pard\pardeftab720\partightenfactor0 + +\f1\i\fs32 \cf2 A Vision for His Mission\ +\pard\pardeftab720\partightenfactor0 + +\f0\i0\fs28 \cf3 Proverbs 29:18\ +\ +\ +\cf2 Two key ideas:\cf3 \ +\pard\pardeftab720\partightenfactor0 + +\fs34 \cf2 1. God's Word gives me VISION. +\f2\b0 \uc0\u8232 +\f0\b 2. VISION changes how I LIVE.\ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\fs36 \cf2 1. I must SEE what God is DOING.\ +\pard\pardeftab720\partightenfactor0 + +\f3\fs28 \cf4 Proverbs 29:18 MSG +\f4\b0 If people can\'92t see what God is doing, they stumble all over themselves; But when they attend to what he reveals, they are most blessed.\ +\ +\ +\ +\ + +\f3\b Proverbs 29:18 ESV +\f4\b0 Where there is no +\f3\b prophetic vision +\f4\b0 the people cast off restraint\'85\ +\ +\ +\ + +\f3\b 1 Samuel 3:1 ESV +\f4\b0 Now the boy Samuel was ministering to the LORD in the presence of Eli. And the word of the LORD was +\f3\b rare in those days +\f4\b0 ; there was no +\f3\b frequent vision. +\f4\b0 \cf2 \ +\ +\ +\ + +\f3\b \cf4 Exodus 32:25 ESV +\f4\b0 \'93And when Moses saw that the people +\f3\b had broken loose +\f4\b0 (for Aaron had let them break loose\'85)\'94\ +\ +\ +\ + +\f3\b 2 Kings 6:17 ESV +\f4\b0 \'93O LORD, please open his eyes that he may see.\'94\ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs36 \cf2 2. I must PERCEIVE what God is SAYING.\ +\pard\pardeftab720\partightenfactor0 + +\f3\fs28 \cf4 Proverbs 29:18b ESV +\f4\b0 but blessed is he who keeps +\f3\b the law. +\f4\b0 \cf2 \ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs34 \cf2 A. I PERCEIVE through TEACHING in the Word. +\f4\b0\fs28 \ +\pard\pardeftab560\partightenfactor0 + +\f3\b \cf5 Ephesians 4:11\'9614 ESV +\f4\b0 And he gave the apostles, the prophets, the evangelists, the shepherds and teachers.\ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs34 \cf2 B. I PERCEIVE through PROPHETIC gifts in the Church.\ +\pard\pardeftab720\partightenfactor0 + +\f3\fs28 \cf4 1 Corinthians 14:1 ESV +\f4\b0 Pursue love, and earnestly desire the spiritual gifts, especially that you may +\f3\b prophesy +\f4\b0 .\ + +\f3\b 1 Corinthians 14:3 ESV +\f4\b0 the one who prophesies speaks to people for their +\f3\b upbuilding +\f4\b0 and +\f3\b encouragement +\f4\b0 and +\f3\b consolation +\f4\b0 .\cf2 \ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs34 \cf2 \ul \ulc2 Prophesy,\ulnone always based on God\'92s Word, brings IMPARTATION to a specific PERSON in a specific SITUATION for a specific REASON.\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f3\fs28 \cf4 1 Corinthians 14:24\'9625 ESV +\f4\b0 But if all prophesy, and an unbeliever or outsider enters, he is convicted by all, he is called to account by all, 25 the secrets of his heart are disclosed, and so, falling on his face, he will worship God and declare that God is really among you.\ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs34 \cf2 \ +\ +\pard\pardeftab720\partightenfactor0 + +\f3\fs28 \cf4 1 Thessalonians 5:20\'9621 ESV +\f4\b0 Do not despise prophecies, 21 but test everything; hold fast what is good.\cf2 \ +\ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs36 \cf2 3. I must be REFORMED by what God REVEALS.\ +\pard\pardeftab720\partightenfactor0 + +\f3\fs28 \cf4 Proverbs 29:18b MSG +\f4\b0 But when they +\f3\b attend to what He reveals +\f4\b0 , they are most blessed. +\f3\b \ + +\f4\b0 \ +\ +\ + +\f3\b Romans 8:29 MSG +\f4\b0 God knew what he was doing from the very beginning. He decided from the outset to shape the lives of those who love him along the same lines as the life of his Son. The Son stands first in the line of humanity he restored. +\f3\b We see the original and intended shape of our lives there in him. +\f4\b0 \ +\ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs34 \cf2 What do we do with what God reveals?\ +\ +A. PRAY it through. +\f2\b0 \ +\pard\pardeftab720\partightenfactor0 + +\f3\b\fs28 \cf4 Psalm 119:18 ESV +\f4\b0 Open my eyes, that I may behold wondrous things out of your law.\cf2 \ + +\f3\b \cf4 Psalm 139:23\'9624 ESV +\f4\b0 Search me, O God, and know my heart! Try me and know my thoughts! 24 And see if there be any grievous way in me, and lead me in the way everlasting!\ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs34 \cf2 B. READ it out. +\f2\b0 \ +\pard\pardeftab720\partightenfactor0 + +\f3\b\fs28 \cf4 Joshua 1:8 ESV +\f4\b0 This Book of the Law shall not depart from your +\f3\b mouth +\f4\b0 , but you shall meditate on it day and night, so that you may be careful to do according to all that is written in it. For then you will make your way prosperous, and then you will have good success.\cf2 \ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs34 \cf2 C. WRITE it down. +\f2\b0 \ +\pard\pardeftab720\partightenfactor0 + +\f3\b\fs28 \cf4 Habakkuk 2:2\'963 ESV +\f4\b0 And the LORD answered me: \'93Write the vision; make it plain on tablets, so he may run who reads it. 3 For still the vision awaits its appointed time; it hastens to the end\'97it will not lie. If it seems slow, wait for it; it will surely come; it will not delay.\'94\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs34 \cf2 D. WALK it out. +\f2\b0 \ +} \ No newline at end of file diff --git a/tests/notes/Slides - Where is Your Faith - Part 8.rtf b/tests/notes/Slides - Where is Your Faith - Part 8.rtf new file mode 100644 index 0000000..99c6c8b --- /dev/null +++ b/tests/notes/Slides - Where is Your Faith - Part 8.rtf @@ -0,0 +1,215 @@ +{\rtf1\ansi\ansicpg1252\cocoartf2870 +\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica-Bold;\f1\fswiss\fcharset0 Helvetica-BoldOblique;\f2\fnil\fcharset0 Verdana; +\f3\fnil\fcharset0 Verdana-Bold;\f4\fswiss\fcharset0 Helvetica-Oblique;\f5\fswiss\fcharset0 Helvetica; +\f6\fnil\fcharset0 Verdana-Italic;} +{\colortbl;\red255\green255\blue255;\red0\green0\blue0;\red0\green0\blue255;\red251\green2\blue255; +\red253\green128\blue8;\red128\green0\blue2;\red0\green23\blue255;\red128\green0\blue23;} +{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;\cssrgb\c0\c12614\c100000;\cssrgb\c100000\c25279\c100000; +\cssrgb\c100000\c57811\c0;\cssrgb\c58072\c6673\c0;\cssrgb\c1229\c23499\c100000;\cssrgb\c58074\c7053\c11604;} +\margl1440\margr1440\vieww13140\viewh12560\viewkind0 +\deftab720 +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs54 \cf2 Where Is Your Faith - Part 8\ +\pard\pardeftab720\partightenfactor0 + +\f1\i\fs32 \cf2 Faith to Fight\ +\pard\pardeftab720\partightenfactor0 + +\f0\i0\fs28 \cf3 Hebrews 11:32-34\ +\ +\ + +\f1\i \cf4 Tesla stunt video on screen behind me. +\f0\i0 \cf3 \ +\pard\pardeftab720\sl280\partightenfactor0 + +\f2\b0 \cf5 \cf6 \ +\ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f3\b \cf7 Hebrews 11:32 ESV +\f2\b0 And what more shall I say? For time would fail me to tell of Gideon, Barak, Samson, Jephthah, of David and Samuel and the prophets\'97\ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs32 \cf2 Faith isn't passive DEPENDENCE on God; it's ACTIVE PARTICIPATION in what God calls us to do.\ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f3\fs28 \cf7 Hebrews 10:38\'9639 ESV +\f2\b0 but my righteous one shall live by faith, and if he shrinks back, my soul has no pleasure in him.\'94 39 But +\f3\b we are not of those who shrink back +\f2\b0 and are destroyed, but of +\f3\b those who have faith and preserve their souls. +\f2\b0 \ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs36 \cf2 1. Faith FIGHTS despite our FLAWS.\ +\pard\pardeftab720\partightenfactor0 + +\f3\fs28 \cf7 Hebrews 11:32\'9633 ESV +\f2\b0 And what more shall I say? For time would fail me to tell of Gideon, Barak, Samson, Jephthah, of David and Samuel and the prophets\'97 33 who through faith\'85\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f4\i \cf4 +\f2 \cf7 \ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs32 \cf2 What is your FLAW? +\f5\b0 \ + +\f0\b 1. FEARFUL INSECURITY - Gideon and Barak\ +2. FLESHLY IMPULSES - David and Samson\ +3. FAMILY DYSFUNCTION - David and Jephthah +\f5\b0 \ + +\f0\b \ +\ +\pard\pardeftab720\partightenfactor0 + +\f3\fs28 \cf7 Galatians 5:17 ESV +\f2\b0 For the desires of the flesh are against the Spirit, and the desires of the Spirit are against the flesh, +\f3\b for these are opposed to each other +\f2\b0 , to keep you from doing the things you want to do.\ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs32 \cf2 \ +\ +\pard\pardeftab720\partightenfactor0 + +\f1\i\fs26 \cf4 (Slides only)\ +\pard\pardeftab720\partightenfactor0 + +\f3\i0\fs28 \cf7 Judges 11:1\'963 ESV +\f2\b0 Now Jephthah the Gileadite was a mighty warrior, but he was the son of a prostitute. Gilead was the father of Jephthah. 2 And Gilead\'92s wife also bore him sons. And when his wife\'92s sons grew up, they drove Jephthah out and said to him, \'93You shall not have an inheritance in our father\'92s house, for you are the son of another woman.\'94 3 Then Jephthah fled from his brothers and lived in the land of Tob, and worthless fellows collected around Jephthah and went out with him.\ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs32 \cf2 \ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f3\fs28 \cf7 Psalm 139:2\'964 ESV +\f2\b0 You know when I sit down and when I rise up; +\f3\b you discern my thoughts from afar. +\f2\b0 3 You search out my path and my lying down and are acquainted with all my ways. 4 Even +\f3\b before a word is on my tongue +\f2\b0 , behold, O LORD, +\f3\b you know it altogether.\ +\pard\pardeftab720\partightenfactor0 + +\f0\fs32 \cf2 \ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\fs36 \cf2 2. Faith FIGHTS despite the ODDS.\ +\pard\pardeftab720\partightenfactor0 + +\f3\fs28 \cf7 Hebrews 11:33 ESV +\f2\b0 who through faith conquered kingdoms, enforced justice, obtained promises, stopped the mouths of lions,\ +\ +\ +\ +\ + +\f3\b Judges 7:2 ESV +\f2\b0 The LORD said to Gideon, \'93The people with you are too many for me to give the Midianites into their hand, lest Israel boast over me, saying, \'91My own hand has +\f3\b \cf7 \ul \ulc7 saved +\f2\b0 \ulnone me.\'92\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f1\i\b\fs26 \cf4 (Slides only)\ +\pard\pardeftab720\partightenfactor0 + +\f3\i0\fs28 \cf7 1 Samuel 17:24 ESV +\f2\b0 All the men of Israel, when they saw the man, fled from him and were much afraid.\ +\ +\ +\ + +\f3\b 1 Samuel 17:45\'9647 ESV +\f2\b0 Then David said to the Philistine, \'93You come to me with a sword and with a spear and with a javelin, +\f3\b but I come to you in the name of the LORD of hosts, +\f2\b0 \'85 47 and +\f3\b that all this assembly may know that the LORD \ul saves\ulnone not with sword and spear. +\f2\b0 For the battle is the LORD\'92s, and he will give you into our hand.\'94\ +\ +\ +\ + +\f3\b 2 Corinthians 10:3\'965 ESV +\f2\b0 For though we walk in the flesh, we are not waging war according to the flesh. 4 For the weapons of our warfare are not of the flesh +\f3\b but have divine power +\f2\b0 to destroy strongholds. 5 We destroy arguments and every lofty opinion raised against the knowledge of God, and take every thought captive to obey Christ,\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f4\i \cf4 +\f2 \cf7 \ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs32 \cf2 Our weapons have DIVINE power. \ +Weapon #1: The NAME OF JESUS - Luke 10:17 +\f5\b0 \ + +\f0\b Weapon #2: The WORD OF GOD - Ephesians 6:17/Hebrews 4:12\ +Weapon #3: The FAMILY OF FAITH - Philippians 1:27 +\f5\b0 \ +\ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 + +\f0\b\fs36 \cf2 3. Faith FIGHTS with STRENGTH God SUPPLIES. +\f5\b0 \ +\pard\pardeftab720\partightenfactor0 + +\f3\b\fs28 \cf7 Hebrews 11:34 ESV +\f2\b0 were +\f3\b made strong out of weakness, +\f2\b0 became mighty in war, put foreign armies to flight.\cf2 \ +\ +\ +\ + +\f3\b \cf7 Ephesians 6:10 ESV +\f2\b0 Finally, be strong in the Lord and in the strength of his might.\ +\ +\ +\ +\pard\pardeftab720\partightenfactor0 +\cf4 On my TV and available by TAP:\ +\pard\pardeftab720\sl360\partightenfactor0 + +\f6\i \cf8 Prayer: Father, I believe in spite of my flaws and failures that you are working in me right now to overcome the world, the flesh and the devil. I depend on your strength and not my own, your grace and not my works. Empower your weakened servant with DIVINE strength to fight in Jesus\'92 Name.\ +\ +\ +\ +\pard\pardeftab720\sl280\partightenfactor0 + +\f3\i0\b \cf2 Sermon in a sentence:\ +\pard\pardeftab720\partightenfactor0 + +\f0\fs32 \cf2 Faith is not passive DEPENDENCE; it\'92s active ENGAGEMENT in the BATTLES of life.} \ No newline at end of file diff --git a/tests/pro/261018 Where Is Your Faith Part 8.pro b/tests/pro/261018 Where Is Your Faith Part 8.pro new file mode 100755 index 0000000..ac23ede Binary files /dev/null and b/tests/pro/261018 Where Is Your Faith Part 8.pro differ diff --git a/tests/run_tests.py b/tests/run_tests.py index f39d306..84f40db 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -112,7 +112,39 @@ def main(): except spelling.Unavailable as e: print("skip typo checks (%s)" % e) - # 7. other campuses: their template names, and who preaches there according to the email + # 7. underlined words in the notes are underlined on the slides (as the operator does by hand) + def underlined_runs(data): + p = deck.C.decode(data, "rv.data.Presentation") + out = [] + for c in deck.C.all(p, "cues"): + a = deck.slide_action(c) + for el in (deck.slide_elements(a) if a is not None else []): + t = deck.element_text(el) + d = deck.C.get(t, "rtf_data") if t is not None else None + for para in (rtf.read(d).paras if d else []): + out += [r.text.strip() for r in para.runs if "ul" in r.state and r.text.strip()] + return sorted(out) + + p8 = notes.parse(open(os.path.join(NOTES, "Slides - Where is Your Faith - Part 8.rtf"), "rb").read()) + got = underlined_runs(deck.build(tpl, p8, "TEST", tpl.speaker())) + hand = underlined_runs(open(os.path.join(PRO, "261018 Where Is Your Faith Part 8.pro"), "rb").read()) + check("part 8 underlines match the hand-fixed deck", got == hand == ["saved", "saves"], "%s vs %s" % (got, hand)) + p7l = notes.parse(open(os.path.join(NOTES, "Slides - 7 Letters - Part 2.rtf"), "rb").read()) + got = underlined_runs(deck.build(tpl, p7l, "TEST", tpl.speaker())) + hand = underlined_runs(open(os.path.join(PRO, "260628 The 7 Letters Part 2.pro"), "rb").read()) + check("7 Letters 2 underlines match the hand-made deck", got == hand, "%s vs %s" % (got, hand)) + tt = notes.parse(open(os.path.join(NOTES, "Slides - Together Tuesday - October 2026.rtf"), "rb").read()) + pt = [s for s in tt.slides if s.kind == "point" and s.lines[0].text.startswith("Prophesy")] + check("underline kept in a point", pt and [x[0] for x in pt[0].lines[0].segs_plain() if x[3]] == ["Prophesy,"], + "%s" % (pt[0].lines[0].segs_plain() if pt else "no point")) + rt = plantext.load(plantext.dump(p8, "x")) + styled = lambda plan: [[(ch,) + tuple(seg[1:]) for seg in l.segs for ch in seg[0] if not ch.isspace()] + for s in plan.slides for l in s.lines] # spaces at bold edges may move + check("underline survives the TextEdit round trip", styled(p8) == styled(rt)) + check("fill-in blanks are not underline marks", plantext.parse_bold("Faith is ______.") == + [("Faith is ______.", False, False, False)]) + + # 8. other campuses: their template names, and who preaches there according to the email import app import settings for n, want in (("_TEMPLATE - DO NOT DELETE 2608 Lower Thirds.pro", True),