Chris Stern's BBEdit Python language module.
diff --git a/Mac/Contrib/BBPy.lm/BBPy.c b/Mac/Contrib/BBPy.lm/BBPy.c
new file mode 100644
index 0000000..85f0dd2
--- /dev/null
+++ b/Mac/Contrib/BBPy.lm/BBPy.c
@@ -0,0 +1,456 @@
+#include <AEDataModel.h>
+
+#define DEBUG 0
+
+#define kComponentSignatureString "BBPy.LM"                 
+#include <Debugging.h>
+
+
+#include <BBLMInterface.h>
+#include <BBXTInterface.h>
+//#include <BBLMTextIterator.h>
+
+#include <ctype.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdarg.h>
+
+#include <Sound.h>
+
+#if DEBUG
+void debugf_(const char* func,const char* fileName,long line, const char*fmt,...)
+{
+	va_list arg;
+	char msg[256];
+	va_start(arg, fmt);
+	vsnprintf(msg,256 ,fmt, arg);
+    DebugAssert(COMPONENT_SIGNATURE, DEBUG_NO_OPTIONS, kComponentSignatureString ": " , msg, nil, fileName, line, 0 );
+
+	//debug_string(msg);
+}
+#define debugf(FMT,...)  debugf_( __FUNCTION__,__FILE__, __LINE__,FMT,__VA_ARGS__);
+#else
+#define debugf(FMT,...) 
+#endif
+
+typedef const char *Str; 
+
+
+enum{
+	kPyBBLMStringSubst =  kBBLMFirstUserRunKind
+};
+
+#define iswordchar(x) (isalnum(x)||x=='_')
+
+
+struct runloc{
+	bool past_gap;
+	long pos;
+	long last_start;
+	unsigned char*p;
+};
+
+char start(struct runloc& r,BBLMParamBlock &pb)
+{
+	r.past_gap = false;
+	r.last_start = pb.fCalcRunParams.fStartOffset;
+	r.pos = pb.fCalcRunParams.fStartOffset;
+	r.p = ((unsigned char*)pb.fText) + pb.fCalcRunParams.fStartOffset;
+	// Adjust for the gap if we¹re not already past it.
+	if ((!r.past_gap) && (r.pos >= pb.fTextGapLocation)){
+		r.p += pb.fTextGapLength;
+		r.past_gap = true;
+	}
+	return *r.p;
+
+}
+
+char nextchar(struct runloc&r,BBLMParamBlock &pb)
+{
+	if ( r.pos< pb.fTextLength){
+		++r.pos;
+		++r.p;
+		if ((!r.past_gap) && (r.pos >= pb.fTextGapLocation)){
+			r.p += pb.fTextGapLength;
+			r.past_gap = true;
+		}
+		return *r.p;
+	}
+	else{
+		return 0;
+	}
+}
+
+bool addRun(BBLMRunCode kind, int  start,int len , const BBLMCallbackBlock& bblm_callbacks)
+{
+	if (len > 0){ // Tie off the code run we were in, unless the length is zero.
+		debugf("Run %d %d:%d", kind, start, start+len-1 );
+		return bblmAddRun(	&bblm_callbacks, 'Pyth',
+							kind, start, len, false);
+							
+	}
+	else{
+		return true;
+	}
+}					
+
+bool addRunBefore (BBLMRunCode kind,struct runloc& r, const BBLMCallbackBlock& bblm_callbacks)
+{
+	bool more_runs = addRun(kind, r.last_start, r.pos - r.last_start, bblm_callbacks);
+	r.last_start =  r.pos;
+	return more_runs;
+}
+
+bool addRunTo (BBLMRunCode kind, struct runloc& r, const BBLMCallbackBlock& bblm_callbacks)
+{
+	bool more_runs = addRun(kind, r.last_start, r.pos - r.last_start+1, bblm_callbacks);
+	r.last_start =  r.pos+1;
+	return more_runs;
+}
+
+
+bool colorstr(	char delim,
+				BBLMParamBlock &pb,
+				struct runloc &r,
+				const BBLMCallbackBlock &bblm_callbacks)
+{
+	bool tripple = false , pers = false, lookup = false, more_runs = true;
+	char c = nextchar(r,pb);
+
+	if (c == delim){
+		c = nextchar(r,pb);
+		if (c == delim){
+			tripple = true;
+			c = nextchar(r,pb);
+		}  
+		else{
+			//double
+			return addRunBefore(kBBLMRunIsSingleString,r,bblm_callbacks);
+		}	
+	}
+	while (c && more_runs){
+		if (pers ){
+			if (isalpha(c)){
+				more_runs = addRunTo(kPyBBLMStringSubst,r,bblm_callbacks);
+			}
+			else if (c == '('){
+				lookup = true;
+			}
+		}
+		pers = false;
+		if (c == delim){
+			if (tripple){
+				if ((c = nextchar(r,pb))== delim && (c = nextchar(r,pb)) == delim){
+					break; // end of tripple-quote.
+				}  
+			}
+			else{
+				break; // end of single-quote.
+			}
+			
+		}
+		else if (c== '\\'){
+			nextchar(r,pb);
+		}
+		else if (c=='\r'||c=='\n'){
+			if (!tripple){	
+				break;
+			}
+		}
+		else if (c=='%'){
+			more_runs = addRunBefore(kBBLMRunIsSingleString,r,bblm_callbacks);
+			pers = true;
+		}
+		else if (c==')' && lookup){
+			more_runs = addRunTo(kPyBBLMStringSubst,r,bblm_callbacks);
+			lookup = false;
+		}
+		c = nextchar(r,pb);
+	}
+	return more_runs && addRunTo(lookup?kPyBBLMStringSubst:kBBLMRunIsSingleString,r,bblm_callbacks);
+}
+
+bool colorcomment(BBLMParamBlock &pb,
+				struct runloc &r,
+				const BBLMCallbackBlock &bblm_callbacks)
+{
+	while (char c = nextchar(r,pb)){
+		if (c=='\r'|| c=='\n'){
+			break;
+		}
+	}
+	return addRunTo(kBBLMRunIsLineComment,r,bblm_callbacks);
+}
+
+void CalculateRuns(BBLMParamBlock &pb,
+			const BBLMCallbackBlock &bblm_callbacks)
+
+{
+	const struct rundesc	*state = NULL;
+	bool more_runs=true;
+		
+	struct runloc r;
+	
+	char c = start(r,pb);
+	
+	while (c && more_runs){
+	loop:
+		// Process a char
+		if (state==NULL){
+			//If we're in the basic 'code' state, check for each interesting char (rundelims[i].start).
+			switch (c){
+			case '\'':
+			case '"': 
+				more_runs = addRunBefore(kBBLMRunIsCode,r,bblm_callbacks);
+				if (more_runs){
+					more_runs = colorstr(c,pb,r,bblm_callbacks);
+				}
+				break;
+			case '#' :
+				more_runs = addRunBefore(kBBLMRunIsCode,r,bblm_callbacks);
+				if (more_runs){
+					more_runs = colorcomment(pb,r,bblm_callbacks); 
+				}
+				break;
+			default:
+				break;
+			}
+
+		}
+		c = nextchar(r,pb);
+	}
+	if (more_runs){
+		addRunBefore(kBBLMRunIsCode,r,bblm_callbacks);
+	}
+	
+
+}
+static void AdjustRange(BBLMParamBlock &params,
+						const BBLMCallbackBlock &callbacks)
+{	
+	DescType language;
+	BBLMRunCode kind;
+	SInt32 charPos;
+	SInt32 length;
+	UInt32 index = params.fAdjustRangeParams.fStartIndex;
+	
+	while(	index > 0 &&
+			bblmGetRun(&callbacks, index, language, kind, charPos, length) &&
+		 	(kind==kPyBBLMStringSubst||kind==kBBLMRunIsSingleString)){
+	 	index--;
+	};
+	 params.fAdjustRangeParams.fStartIndex = index;
+}
+
+
+// The next couple funcs process the text of a file assumming it's in 1 piece in memory,
+// so they may not be called from CalculateRuns.
+
+bool matchword(BBLMParamBlock &pb, const char *pat ,unsigned long *pos)
+{	
+	const char *asciText = (const char *) (pb.fTextIsUnicode?NULL:pb.fText);
+
+	int i;
+	for (i=0; pat[i]; i++){
+		if (*pos+i>=pb.fTextLength){
+			return false;
+		}
+		if (asciText[*pos+i] != pat[i]){
+			return false;
+		}
+	}
+	if ((*pos+i<pb.fTextLength)&&iswordchar(asciText[*pos+i])){
+		return false;
+	}
+	*pos+=i;
+	return true;
+}	
+
+int matchindent(BBLMParamBlock &pb, UInt32 *pos)
+{	
+	const char *asciText = (const char *) (pb.fTextIsUnicode?NULL:pb.fText);
+	int indent=0;
+		
+	while(*pos<pb.fTextLength){
+		switch (/*(char)(pb.fTextIsUnicode?uniText[pos]:*/asciText[*pos]/*)*/){
+		case ' ':
+			++*pos;
+			indent++;
+			break;	
+		case '\t':
+			++*pos;		
+			indent+=8;
+			break;
+		case '#':
+			return -1;
+			break;
+		default:
+			return indent;
+			break;
+		}
+	}	
+}
+
+
+void eat_line(BBLMParamBlock &pb, unsigned long* pos)
+{
+	const char *asciText = (const char *) (pb.fTextIsUnicode?NULL:pb.fText);
+	while (asciText[*pos]!='\r' && asciText[*pos]!='\n' && *pos<pb.fTextLength) {++*pos;}
+	while ((asciText[*pos]=='\r' || asciText[*pos]=='\n') && *pos<pb.fTextLength) {++*pos;}
+
+}
+
+void addItem(BBLMParamBlock &pb, UInt32 pos, int nest, BBLMFunctionKinds kind,
+			const BBLMCallbackBlock *bblm_callbacks)
+{
+	UInt32 funcstartpos = pos;
+	UInt32 funcnamelen=0;
+	UInt32 offset=0;
+	const char *asciText = (const char *) pb.fText;
+	UInt32 index;
+	OSErr err;
+	
+	while (isspace(asciText[pos]) && pos<pb.fTextLength) {++pos;}
+	UInt32 fnamestart = pos;
+	while ((isalnum(asciText[pos])||asciText[pos]=='_') && pos<pb.fTextLength) {pos++; funcnamelen++;}
+	
+	err = bblmAddTokenToBuffer(	bblm_callbacks, 
+								pb.fFcnParams.fTokenBuffer,
+								(void*)&asciText[fnamestart],
+								funcnamelen,
+								pb.fTextIsUnicode,
+								&offset);
+	BBLMProcInfo procInfo; 
+	procInfo.fFunctionStart = fnamestart;	//	char offset in file of first character of function
+	procInfo.fFunctionEnd = pos;	//	char offset of last character of function
+	
+	procInfo.fSelStart = fnamestart;		//	first character to select when choosing function
+	procInfo.fSelEnd = pos;		//	last character to select when choosing function
+	
+	procInfo.fFirstChar = fnamestart;		//	first character to make visible when choosing function
+	
+	procInfo.fKind = kind;
+	
+	procInfo.fIndentLevel = nest;	//	indentation level of token
+	procInfo.fFlags = 0;			//	token flags (see BBLMFunctionFlags)
+	procInfo.fNameStart = offset;		//	char offset in token buffer of token name
+	procInfo.fNameLength = funcnamelen;	//	length of token name
+																
+	err = bblmAddFunctionToList(bblm_callbacks,	
+								pb.fFcnParams.fFcnList,
+								procInfo,
+								&index);
+}
+
+
+
+enum{
+	maxnest=5
+};
+
+void ScanForFunctions(BBLMParamBlock &pb,
+			const BBLMCallbackBlock &bblm_callbacks)
+{
+
+	const char *asciText = (const char *) (pb.fTextIsUnicode?NULL:pb.fText);
+	UniCharPtr uniText = (UniCharPtr) (pb.fTextIsUnicode?pb.fText:NULL);
+	
+	int indents[maxnest]= {0};
+	int nest = 0;
+	
+	UInt32 pos=0; // current character offset
+
+	
+	while (pos<pb.fTextLength){
+		
+		int indent = matchindent(pb, &pos);
+		
+		if (indent >= 0){
+			for (int i=0; i <= nest; i++){
+				if (indent<=indents[i]){
+					nest = i;
+					indents[nest]=indent;
+					goto x;
+				}
+			}
+			indents[++nest]=indent;
+			x:
+		
+			if (matchword(pb,"def",&pos)){
+				addItem( pb, pos, nest, kBBLMFunctionMark, &bblm_callbacks);
+			}
+			else if (matchword(pb, "class", &pos)){
+				addItem( pb, pos, nest, kBBLMTypedef, &bblm_callbacks);
+			}
+		}
+		eat_line(pb,&pos);
+	}
+	
+}
+
+OSErr main(	BBLMParamBlock &params,
+			const BBLMCallbackBlock &bblm_callbacks,
+			const BBXTCallbackBlock &bbxt_callbacks)
+{
+	OSErr result;
+
+	if ((params.fSignature != kBBLMParamBlockSignature) ||
+		(params.fLength < sizeof(BBLMParamBlock)))
+	{
+		return paramErr;
+	}
+	
+	switch (params.fMessage)
+	{
+		case kBBLMInitMessage:
+		case kBBLMDisposeMessage:
+		{
+			result = noErr;	// nothing to do
+			break;
+		}
+		
+		case kBBLMCalculateRunsMessage:
+			CalculateRuns(params, bblm_callbacks);
+			result = noErr;
+			break;
+
+		case kBBLMScanForFunctionsMessage:
+			ScanForFunctions(params, bblm_callbacks);
+			result = noErr;
+			break;
+
+		case kBBLMAdjustRangeMessage:
+			AdjustRange(params, bblm_callbacks);
+			result = noErr;
+			break;
+		
+		case kBBLMMapRunKindToColorCodeMessage:
+			switch (params.fMapRunParams.fRunKind){
+			case kPyBBLMStringSubst:
+				params.fMapRunParams.fColorCode = kBBLMSGMLAttributeNameColor;
+				params.fMapRunParams.fMapped =	true;
+				break;	
+			default:
+				params.fMapRunParams.fMapped =	false;
+			}
+			result = noErr;
+			break;
+
+		case kBBLMEscapeStringMessage:
+		case kBBLMAdjustEndMessage:
+		case kBBLMMapColorCodeToColorMessage:
+		case kBBLMSetCategoriesMessage:
+		case kBBLMMatchKeywordMessage:
+		{
+			result = userCanceledErr;
+			break;
+		}
+			
+		default:
+		{
+			result = paramErr;
+			break;
+		}
+	}
+	return result;	
+}
\ No newline at end of file